feat: knowledge base with URL import, ADB case-sensitivity fix, and RAG enhancements
- Add Knowledge Base (Base de Conhecimento) with file upload + URL import - URL content extraction: HTML stripping, remote PDF parsing - Auto-resolve embedding config from OCI credentials - Case-insensitive ADB table matching with quoted identifiers - Table validation endpoint, report_date metadata, purge & re-embed flow - Embeddings tab cleanup, PyPDF2 dependency, README updated to v2.3
This commit is contained in:
@@ -526,7 +526,7 @@ const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],g
|
||||
chatParams:{temperature:1,max_tokens:6000,top_p:0.95,top_k:1,frequency_penalty:0,presence_penalty:0},chatPanel:'',chatUseTools:true,
|
||||
chatPrompts:[],editingPrompt:null,trackingReportId:null,ociRegions:{},rptSelRegions:[],rptRegionsOpen:false,rptRegionFilter:'',
|
||||
rptOciOpen:false,rptOciFilter:'',rptOciVal:'',rptRselOpen:false,rptRselFilter:'',rptRselVal:'',ociFormRegOpen:false,ociFormRegFilter:'',ociFormRegVal:'',
|
||||
rptLevel:2,rptObp:false,rptRaw:false,rptRedact:false,reportFiles:{},dlExpandedRid:null,dlTenancyFilter:'',
|
||||
rptLevel:2,rptObp:false,rptRaw:false,rptRedact:false,reportFiles:{},dlExpandedRid:null,dlTenancyFilter:'',dlSectionFilter:'',
|
||||
cisVer:null,cisCheckResult:null,cisUpdating:false,rptHistFilter:'',rptKpi:null,rptKpiLoading:false,chatFiles:[],
|
||||
tfMsgs:[],tfSid:null,tfModel:'',tfOci:'',tfRegion:'',tfCompartment:'',tfPlan:[],tfCode:'',tfPanel:'',
|
||||
tfWs:null,tfWsList:[],tfPlanOut:'',tfApplyOut:'',tfDestroyOut:'',tfStatus:'draft',tfRunning:false,tfConfirmDest:false,tfMdOpen:false,
|
||||
@@ -647,7 +647,7 @@ function rChat(){
|
||||
else if(S.chatModel){const mi=S.models[S.chatModel];curLabel=mi?mi.name:S.chatModel}
|
||||
const isDirect=S.chatModel&&!S.chatModel.startsWith('cfg:');
|
||||
const ociSel=isDirect?`<select id="coci" onchange="chatOciChanged()" style="max-width:200px"><option value="">Credencial OCI...</option>${S.ociCfg.map(c=>`<option value="${c.id}" ${S.chatOci===c.id?'selected':''}>${c.tenancy_name} (${c.region})</option>`).join('')}</select>`:'';
|
||||
const ragBadge=S.adbCfg.some(c=>c.genai_config_id&&c.is_active)?'<span class="tag" style="background:var(--gnl);color:var(--gn);font-size:.62rem">RAG</span>':'';
|
||||
const ragBadge=S.adbCfg.some(c=>c.is_active)&&(S.genaiCfg.length||S.ociCfg.length)?'<span class="tag" style="background:var(--gnl);color:var(--gn);font-size:.62rem">RAG</span>':'';
|
||||
const toolCount=S.mcpSvr.filter(m=>m.is_active&&Array.isArray(m.tools)&&m.tools.length).reduce((a,m)=>a+m.tools.length,0);
|
||||
let sideContent='';
|
||||
if(S.chatPanel==='config')sideContent=rChatConfig(isDirect,toolCount);
|
||||
@@ -1729,6 +1729,7 @@ async function expLoadRes(){
|
||||
function renderExpData(d){if(!d)return'<div class="emp"><p>Selecione um compartment na árvore.</p></div>';
|
||||
if(d.error)return`<div class="al al-e">${d.error}</div>`;if(!Array.isArray(d)||!d.length)return'<div class="emp"><p>Nenhum recurso encontrado neste compartment.</p></div>';
|
||||
if(S.expResType==='security_lists')return rExpSecLists(d);
|
||||
if(S.expResType==='nsgs')return rExpNsgs(d);
|
||||
if(S.expResType==='route_tables')return rExpRouteTables(d);
|
||||
const isInst=S.expResType==='instances';
|
||||
const isAdb=S.expResType==='databases';
|
||||
@@ -1750,18 +1751,119 @@ function rExpSecLists(d){
|
||||
const ingress=rules.filter(r=>r.direction==='ingress');
|
||||
const egress=rules.filter(r=>r.direction==='egress');
|
||||
const expanded=S._expExpanded&&S._expExpanded[idx];
|
||||
const showForm=S._slAddForm===sl.id;
|
||||
return`<div class="exp-c" style="max-width:100%"><div style="display:flex;justify-content:space-between;align-items:center;cursor:pointer" onclick="S._expExpanded=S._expExpanded||{};S._expExpanded[${idx}]=!S._expExpanded[${idx}];R()">
|
||||
<div><strong style="display:inline">${sl.display_name}</strong> <span class="badge ${sl.lifecycle_state==='AVAILABLE'?'b-pass':'b-pend'}">${sl.lifecycle_state}</span></div>
|
||||
<div style="font-size:.66rem;color:var(--t4)">${expanded?'▼':'▶'} ${ingress.length} ingress · ${egress.length} egress</div></div>
|
||||
<div class="em" style="margin-top:.3rem">id: ${sl.id}</div>
|
||||
${expanded?`<div style="margin-top:.5rem">${ingress.length?`<div style="font-size:.68rem;font-weight:600;color:var(--t2);margin:.4rem 0 .2rem">↓ Ingress (${ingress.length})</div>
|
||||
<table style="width:100%;font-size:.66rem;border-collapse:collapse"><thead><tr style="background:var(--bg3);text-align:left"><th style="padding:.25rem .4rem">Protocolo</th><th style="padding:.25rem .4rem">Origem</th><th style="padding:.25rem .4rem">Portas</th><th style="padding:.25rem .4rem">Stateless</th><th style="padding:.25rem .4rem">Descrição</th></tr></thead><tbody>
|
||||
${ingress.map(r=>`<tr style="border-top:1px solid var(--bd)"><td style="padding:.25rem .4rem;font-weight:600">${r.protocol}</td><td style="padding:.25rem .4rem;font-family:var(--fm);font-size:.62rem">${r.source_dest}</td><td style="padding:.25rem .4rem">${r.ports||'ALL'}</td><td style="padding:.25rem .4rem">${r.stateless?'Sim':'Não'}</td><td style="padding:.25rem .4rem;color:var(--t4);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${(r.description||'').replace(/"/g,'"')}">${r.description||'—'}</td></tr>`).join('')}
|
||||
${expanded?`<div style="margin-top:.5rem">
|
||||
<div style="display:flex;justify-content:flex-end;margin-bottom:.4rem"><button class="btn bp bsm" onclick="event.stopPropagation();S._slAddForm=S._slAddForm==='${sl.id}'?null:'${sl.id}';R()" style="font-size:.62rem">${showForm?'✕ Cancelar':'+ Adicionar Regra'}</button></div>
|
||||
${showForm?`<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:.6rem;margin-bottom:.6rem" onclick="event.stopPropagation()">
|
||||
<div style="font-size:.7rem;font-weight:600;color:var(--t2);margin-bottom:.4rem">${IC.shield} Nova Regra</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:.4rem">
|
||||
<div class="ig"><label style="font-size:.62rem">Direção</label><select id="slDir" style="font-size:.72rem"><option value="ingress">Ingress</option><option value="egress">Egress</option></select></div>
|
||||
<div class="ig"><label style="font-size:.62rem">Protocolo</label><select id="slProto" style="font-size:.72rem"><option value="6">TCP</option><option value="17">UDP</option><option value="1">ICMP</option><option value="all">ALL</option></select></div>
|
||||
<div class="ig"><label style="font-size:.62rem">Stateless</label><select id="slStateless" style="font-size:.72rem"><option value="false">Não</option><option value="true">Sim</option></select></div>
|
||||
<div class="ig"><label style="font-size:.62rem">Origem/Destino (CIDR)</label><input id="slCidr" style="font-size:.72rem" placeholder="0.0.0.0/0" value="0.0.0.0/0"></div>
|
||||
<div class="ig"><label style="font-size:.62rem">Porta Início</label><input id="slPortMin" type="number" style="font-size:.72rem" placeholder="ex: 443"></div>
|
||||
<div class="ig"><label style="font-size:.62rem">Porta Fim</label><input id="slPortMax" type="number" style="font-size:.72rem" placeholder="ex: 443"></div>
|
||||
</div>
|
||||
<div class="ig" style="margin-top:.3rem"><label style="font-size:.62rem">Descrição</label><input id="slDesc" style="font-size:.72rem" placeholder="Descrição da regra"></div>
|
||||
<div style="display:flex;gap:.3rem;margin-top:.5rem;justify-content:flex-end"><button class="btn bp bsm" onclick="slAddRule('${sl.id}')" style="font-size:.64rem">Salvar Regra</button>
|
||||
<button class="btn bs bsm" onclick="slAddMyIp('${sl.id}')" style="font-size:.64rem" title="Detecta seu IP público e adiciona como regra">${IC.globe} Meu IP</button></div>
|
||||
<div id="slFormMsg" style="margin-top:.3rem"></div>
|
||||
</div>`:''}
|
||||
${ingress.length?`<div style="font-size:.68rem;font-weight:600;color:var(--t2);margin:.4rem 0 .2rem">↓ Ingress (${ingress.length})</div>
|
||||
<table style="width:100%;font-size:.66rem;border-collapse:collapse"><thead><tr style="background:var(--bg3);text-align:left"><th style="padding:.25rem .4rem">Protocolo</th><th style="padding:.25rem .4rem">Origem</th><th style="padding:.25rem .4rem">Portas</th><th style="padding:.25rem .4rem">Stateless</th><th style="padding:.25rem .4rem">Descrição</th><th style="padding:.25rem .4rem;width:32px"></th></tr></thead><tbody>
|
||||
${ingress.map(r=>`<tr style="border-top:1px solid var(--bd)"><td style="padding:.25rem .4rem;font-weight:600">${r.protocol}</td><td style="padding:.25rem .4rem;font-family:var(--fm);font-size:.62rem">${r.source_dest}</td><td style="padding:.25rem .4rem">${r.ports||'ALL'}</td><td style="padding:.25rem .4rem">${r.stateless?'Sim':'Não'}</td><td style="padding:.25rem .4rem;color:var(--t4);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${(r.description||'').replace(/"/g,'"')}">${r.description||'—'}</td><td style="padding:.25rem .2rem"><button class="btn bd bsm" style="font-size:.55rem;padding:1px 5px" onclick="event.stopPropagation();slRemoveRule('${sl.id}','ingress',${r.rule_index})" title="Remover regra">✕</button></td></tr>`).join('')}
|
||||
</tbody></table>`:''}
|
||||
${egress.length?`<div style="font-size:.68rem;font-weight:600;color:var(--t2);margin:.4rem 0 .2rem">↑ Egress (${egress.length})</div>
|
||||
<table style="width:100%;font-size:.66rem;border-collapse:collapse"><thead><tr style="background:var(--bg3);text-align:left"><th style="padding:.25rem .4rem">Protocolo</th><th style="padding:.25rem .4rem">Destino</th><th style="padding:.25rem .4rem">Portas</th><th style="padding:.25rem .4rem">Stateless</th><th style="padding:.25rem .4rem">Descrição</th></tr></thead><tbody>
|
||||
${egress.map(r=>`<tr style="border-top:1px solid var(--bd)"><td style="padding:.25rem .4rem;font-weight:600">${r.protocol}</td><td style="padding:.25rem .4rem;font-family:var(--fm);font-size:.62rem">${r.source_dest}</td><td style="padding:.25rem .4rem">${r.ports||'ALL'}</td><td style="padding:.25rem .4rem">${r.stateless?'Sim':'Não'}</td><td style="padding:.25rem .4rem;color:var(--t4);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${(r.description||'').replace(/"/g,'"')}">${r.description||'—'}</td></tr>`).join('')}
|
||||
<table style="width:100%;font-size:.66rem;border-collapse:collapse"><thead><tr style="background:var(--bg3);text-align:left"><th style="padding:.25rem .4rem">Protocolo</th><th style="padding:.25rem .4rem">Destino</th><th style="padding:.25rem .4rem">Portas</th><th style="padding:.25rem .4rem">Stateless</th><th style="padding:.25rem .4rem">Descrição</th><th style="padding:.25rem .4rem;width:32px"></th></tr></thead><tbody>
|
||||
${egress.map(r=>`<tr style="border-top:1px solid var(--bd)"><td style="padding:.25rem .4rem;font-weight:600">${r.protocol}</td><td style="padding:.25rem .4rem;font-family:var(--fm);font-size:.62rem">${r.source_dest}</td><td style="padding:.25rem .4rem">${r.ports||'ALL'}</td><td style="padding:.25rem .4rem">${r.stateless?'Sim':'Não'}</td><td style="padding:.25rem .4rem;color:var(--t4);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${(r.description||'').replace(/"/g,'"')}">${r.description||'—'}</td><td style="padding:.25rem .2rem"><button class="btn bd bsm" style="font-size:.55rem;padding:1px 5px" onclick="event.stopPropagation();slRemoveRule('${sl.id}','egress',${r.rule_index})" title="Remover regra">✕</button></td></tr>`).join('')}
|
||||
</tbody></table>`:''}</div>`:''}</div>`}).join('')}`}
|
||||
async function slAddRule(slId){
|
||||
const dir=document.getElementById('slDir').value;
|
||||
const proto=document.getElementById('slProto').value;
|
||||
const cidr=document.getElementById('slCidr').value.trim();
|
||||
const portMin=document.getElementById('slPortMin').value;
|
||||
const portMax=document.getElementById('slPortMax').value;
|
||||
const desc=document.getElementById('slDesc').value;
|
||||
const stateless=document.getElementById('slStateless').value==='true';
|
||||
if(!cidr){sm('slFormMsg','Informe o CIDR.','e');return}
|
||||
const region=S.expSelRegions.length===1?S.expSelRegions[0]:null;
|
||||
const body={direction:dir,protocol:proto,source_dest:cidr,description:desc,is_stateless:stateless,region:region||undefined};
|
||||
if(proto!=='all'&&proto!=='1'&&portMin)Object.assign(body,{port_min:parseInt(portMin),port_max:parseInt(portMax||portMin)});
|
||||
try{const d=await $api('/oci/explore/'+S.expCfg+'/security_lists/'+slId+'/rules',{method:'POST',body});
|
||||
sm('slFormMsg',IC.ok+' '+d.message,'s');S._slAddForm=null;await expLoadRes()}catch(e){sm('slFormMsg',e.message,'e')}}
|
||||
async function slRemoveRule(slId,direction,ruleIndex){
|
||||
if(!confirm('Remover esta regra da Security List?'))return;
|
||||
const region=S.expSelRegions.length===1?S.expSelRegions[0]:null;
|
||||
try{await $api('/oci/explore/'+S.expCfg+'/security_lists/'+slId+'/rules',{method:'DELETE',body:{direction,rule_index:ruleIndex,region:region||undefined}});await expLoadRes()}catch(e){alert('Erro: '+e.message)}}
|
||||
async function slAddMyIp(slId){
|
||||
try{const resp=await fetch('https://api.ipify.org?format=json');const data=await resp.json();const myIp=data.ip+'/32';
|
||||
document.getElementById('slCidr').value=myIp;
|
||||
document.getElementById('slDesc').value='Acesso temporário - '+myIp;
|
||||
sm('slFormMsg',IC.ok+' IP detectado: '+myIp+' — ajuste porta e clique Salvar','s')}catch(e){sm('slFormMsg','Não foi possível detectar o IP: '+e.message,'e')}}
|
||||
function rExpNsgs(d){
|
||||
return`<div style="font-size:.74rem;color:var(--t4);margin-bottom:.5rem">${d.length} NSG(s)</div>${d.map((nsg,idx)=>{
|
||||
const rules=nsg.rules||[];
|
||||
const ingress=rules.filter(r=>r.direction==='INGRESS');
|
||||
const egress=rules.filter(r=>r.direction==='EGRESS');
|
||||
const expanded=S._expExpanded&&S._expExpanded['nsg'+idx];
|
||||
const showForm=S._nsgAddForm===nsg.id;
|
||||
return`<div class="exp-c" style="max-width:100%"><div style="display:flex;justify-content:space-between;align-items:center;cursor:pointer" onclick="S._expExpanded=S._expExpanded||{};S._expExpanded['nsg'+${idx}]=!S._expExpanded['nsg'+${idx}];R()">
|
||||
<div><strong style="display:inline">${nsg.display_name}</strong> <span class="badge ${nsg.lifecycle_state==='AVAILABLE'?'b-pass':'b-pend'}">${nsg.lifecycle_state}</span></div>
|
||||
<div style="font-size:.66rem;color:var(--t4)">${expanded?'▼':'▶'} ${ingress.length} ingress · ${egress.length} egress</div></div>
|
||||
<div class="em" style="margin-top:.3rem">id: ${nsg.id}</div>
|
||||
${expanded?`<div style="margin-top:.5rem">
|
||||
<div style="display:flex;justify-content:flex-end;margin-bottom:.4rem"><button class="btn bp bsm" onclick="event.stopPropagation();S._nsgAddForm=S._nsgAddForm==='${nsg.id}'?null:'${nsg.id}';R()" style="font-size:.62rem">${showForm?'✕ Cancelar':'+ Adicionar Regra'}</button></div>
|
||||
${showForm?`<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:.6rem;margin-bottom:.6rem" onclick="event.stopPropagation()">
|
||||
<div style="font-size:.7rem;font-weight:600;color:var(--t2);margin-bottom:.4rem">${IC.shield} Nova Regra</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:.4rem">
|
||||
<div class="ig"><label style="font-size:.62rem">Direção</label><select id="nsgDir" style="font-size:.72rem"><option value="INGRESS">Ingress</option><option value="EGRESS">Egress</option></select></div>
|
||||
<div class="ig"><label style="font-size:.62rem">Protocolo</label><select id="nsgProto" style="font-size:.72rem"><option value="6">TCP</option><option value="17">UDP</option><option value="1">ICMP</option><option value="all">ALL</option></select></div>
|
||||
<div class="ig"><label style="font-size:.62rem">Stateless</label><select id="nsgStateless" style="font-size:.72rem"><option value="false">Não</option><option value="true">Sim</option></select></div>
|
||||
<div class="ig"><label style="font-size:.62rem">Origem/Destino (CIDR)</label><input id="nsgCidr" style="font-size:.72rem" placeholder="0.0.0.0/0" value="0.0.0.0/0"></div>
|
||||
<div class="ig"><label style="font-size:.62rem">Porta Início</label><input id="nsgPortMin" type="number" style="font-size:.72rem" placeholder="ex: 443"></div>
|
||||
<div class="ig"><label style="font-size:.62rem">Porta Fim</label><input id="nsgPortMax" type="number" style="font-size:.72rem" placeholder="ex: 443"></div>
|
||||
</div>
|
||||
<div class="ig" style="margin-top:.3rem"><label style="font-size:.62rem">Descrição</label><input id="nsgDesc" style="font-size:.72rem" placeholder="Descrição da regra"></div>
|
||||
<div style="display:flex;gap:.3rem;margin-top:.5rem;justify-content:flex-end"><button class="btn bp bsm" onclick="nsgAddRule('${nsg.id}')" style="font-size:.64rem">Salvar Regra</button>
|
||||
<button class="btn bs bsm" onclick="nsgAddMyIp('${nsg.id}')" style="font-size:.64rem" title="Detecta seu IP público e adiciona como regra">${IC.globe} Meu IP</button></div>
|
||||
<div id="nsgFormMsg" style="margin-top:.3rem"></div>
|
||||
</div>`:''}
|
||||
${ingress.length?`<div style="font-size:.68rem;font-weight:600;color:var(--t2);margin:.4rem 0 .2rem">↓ Ingress (${ingress.length})</div>
|
||||
<table style="width:100%;font-size:.66rem;border-collapse:collapse"><thead><tr style="background:var(--bg3);text-align:left"><th style="padding:.25rem .4rem">Protocolo</th><th style="padding:.25rem .4rem">Origem</th><th style="padding:.25rem .4rem">Portas</th><th style="padding:.25rem .4rem">Stateless</th><th style="padding:.25rem .4rem">Descrição</th><th style="padding:.25rem .4rem;width:32px"></th></tr></thead><tbody>
|
||||
${ingress.map(r=>`<tr style="border-top:1px solid var(--bd)"><td style="padding:.25rem .4rem;font-weight:600">${r.protocol}</td><td style="padding:.25rem .4rem;font-family:var(--fm);font-size:.62rem">${r.source_dest}</td><td style="padding:.25rem .4rem">${r.ports||'ALL'}</td><td style="padding:.25rem .4rem">${r.is_stateless?'Sim':'Não'}</td><td style="padding:.25rem .4rem;color:var(--t4);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${(r.description||'').replace(/"/g,'"')}">${r.description||'—'}</td><td style="padding:.25rem .2rem"><button class="btn bd bsm" style="font-size:.55rem;padding:1px 5px" onclick="event.stopPropagation();nsgRemoveRule('${nsg.id}','${r.id}')" title="Remover regra">✕</button></td></tr>`).join('')}
|
||||
</tbody></table>`:''}
|
||||
${egress.length?`<div style="font-size:.68rem;font-weight:600;color:var(--t2);margin:.4rem 0 .2rem">↑ Egress (${egress.length})</div>
|
||||
<table style="width:100%;font-size:.66rem;border-collapse:collapse"><thead><tr style="background:var(--bg3);text-align:left"><th style="padding:.25rem .4rem">Protocolo</th><th style="padding:.25rem .4rem">Destino</th><th style="padding:.25rem .4rem">Portas</th><th style="padding:.25rem .4rem">Stateless</th><th style="padding:.25rem .4rem">Descrição</th><th style="padding:.25rem .4rem;width:32px"></th></tr></thead><tbody>
|
||||
${egress.map(r=>`<tr style="border-top:1px solid var(--bd)"><td style="padding:.25rem .4rem;font-weight:600">${r.protocol}</td><td style="padding:.25rem .4rem;font-family:var(--fm);font-size:.62rem">${r.source_dest}</td><td style="padding:.25rem .4rem">${r.ports||'ALL'}</td><td style="padding:.25rem .4rem">${r.is_stateless?'Sim':'Não'}</td><td style="padding:.25rem .4rem;color:var(--t4);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${(r.description||'').replace(/"/g,'"')}">${r.description||'—'}</td><td style="padding:.25rem .2rem"><button class="btn bd bsm" style="font-size:.55rem;padding:1px 5px" onclick="event.stopPropagation();nsgRemoveRule('${nsg.id}','${r.id}')" title="Remover regra">✕</button></td></tr>`).join('')}
|
||||
</tbody></table>`:''}</div>`:''}</div>`}).join('')}`}
|
||||
async function nsgAddRule(nsgId){
|
||||
const dir=document.getElementById('nsgDir').value;
|
||||
const proto=document.getElementById('nsgProto').value;
|
||||
const cidr=document.getElementById('nsgCidr').value.trim();
|
||||
const portMin=document.getElementById('nsgPortMin').value;
|
||||
const portMax=document.getElementById('nsgPortMax').value;
|
||||
const desc=document.getElementById('nsgDesc').value;
|
||||
const stateless=document.getElementById('nsgStateless').value==='true';
|
||||
if(!cidr){sm('nsgFormMsg','Informe o CIDR.','e');return}
|
||||
const region=S.expSelRegions.length===1?S.expSelRegions[0]:null;
|
||||
const body={direction:dir,protocol:proto,source_dest:cidr,source_dest_type:'CIDR_BLOCK',description:desc,is_stateless:stateless,region:region||undefined};
|
||||
if(proto!=='all'&&proto!=='1'&&portMin)Object.assign(body,{port_min:parseInt(portMin),port_max:parseInt(portMax||portMin)});
|
||||
try{const d=await $api('/oci/explore/'+S.expCfg+'/nsgs/'+nsgId+'/rules',{method:'POST',body});
|
||||
sm('nsgFormMsg',IC.ok+' '+d.message,'s');S._nsgAddForm=null;await expLoadRes()}catch(e){sm('nsgFormMsg',e.message,'e')}}
|
||||
async function nsgRemoveRule(nsgId,ruleId){
|
||||
if(!confirm('Remover esta regra do NSG?'))return;
|
||||
const region=S.expSelRegions.length===1?S.expSelRegions[0]:null;
|
||||
const q=region?'?region='+region:'';
|
||||
try{await $api('/oci/explore/'+S.expCfg+'/nsgs/'+nsgId+'/rules/'+ruleId+q,{method:'DELETE'});await expLoadRes()}catch(e){alert('Erro: '+e.message)}}
|
||||
async function nsgAddMyIp(nsgId){
|
||||
try{const resp=await fetch('https://api.ipify.org?format=json');const data=await resp.json();const myIp=data.ip+'/32';
|
||||
document.getElementById('nsgCidr').value=myIp;
|
||||
document.getElementById('nsgDesc').value='Acesso temporário - '+myIp;
|
||||
sm('nsgFormMsg',IC.ok+' IP detectado: '+myIp+' — ajuste porta e clique Salvar','s')}catch(e){sm('nsgFormMsg','Não foi possível detectar o IP: '+e.message,'e')}}
|
||||
function rExpRouteTables(d){
|
||||
return`<div style="font-size:.74rem;color:var(--t4);margin-bottom:.5rem">${d.length} route table(s)</div>${d.map((rt,idx)=>{
|
||||
const routes=rt.routes||[];
|
||||
@@ -2026,22 +2128,98 @@ ${!filtered.length?'<div class="cd"><div class="emp"><div class="eic">${IC.folde
|
||||
${isExp?fHtml:''}</div>`}).join('')}</div>`}`}
|
||||
async function toggleFiles(rid){if(S.dlExpandedRid===rid){S.dlExpandedRid=null;R();return}
|
||||
S.dlExpandedRid=rid;if(!S.reportFiles[rid]){try{S.reportFiles[rid]=await $api('/reports/'+rid+'/files')}catch(e){S.reportFiles[rid]=[]}}R()}
|
||||
function _dlSection(name){
|
||||
// Extract CIS section from filename: cis_Networking_2-1.csv → Networking
|
||||
const m=name.match(/^(?:cis|obp)_([A-Za-z_]+?)_\d/);
|
||||
if(m)return m[1].replace(/_/g,' ');
|
||||
if(name.includes('summary'))return 'Summary';
|
||||
if(name.includes('error'))return 'Error';
|
||||
return 'Other';
|
||||
}
|
||||
const DL_SECTION_TABLE={
|
||||
'Identity and Access Management':'identityandaccess',
|
||||
'Networking':'networking',
|
||||
'Compute':'computeinstances',
|
||||
'Logging and Monitoring':'loggingandmonitoring',
|
||||
'Storage Object Storage':'objectstorage',
|
||||
'Storage Block Volumes':'storageblockvolume',
|
||||
'Storage File Storage Service':'filestorageservice',
|
||||
'Asset Management':'assetmanagement',
|
||||
'Summary':'summaryreportcsvvector'
|
||||
};
|
||||
function renderFileList(rid,files){if(!files.length)return'<div style="padding:.8rem;color:var(--t4);font-size:.72rem;text-align:center">Nenhum arquivo encontrado</div>';
|
||||
const 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: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]||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=''">
|
||||
const secIcons={'Identity and Access Management':IC.lock,'Networking':IC.globe,'Compute':IC.server,'Logging and Monitoring':IC.log,'Storage Object Storage':IC.db,'Asset Management':IC.pkg,'Summary':IC.chart,'Error':IC.err,'Other':IC.file};
|
||||
// Group files by section
|
||||
const bySection={};files.forEach(f=>{const s=_dlSection(f.file_name);if(!bySection[s])bySection[s]=[];bySection[s].push(f)});
|
||||
const sectionOrder=Object.keys(bySection).sort((a,b)=>{if(a==='Summary')return -1;if(b==='Summary')return 1;if(a==='Other')return 1;if(b==='Other')return -1;return a.localeCompare(b)});
|
||||
const hasAdb=S.adbCfg.length>0&&(S.genaiCfg.length>0||S.ociCfg.length>0);
|
||||
let h='<div style="border-top:1px solid var(--bd);padding:.65rem .8rem;background:var(--bg2)">';
|
||||
// Header with total + embed all button
|
||||
h+=`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.6rem">`;
|
||||
h+=`<span style="font-size:.72rem;color:var(--t3)">${files.length} arquivo(s) em ${sectionOrder.length} seções</span>`;
|
||||
if(hasAdb)h+=`<button class="btn bp bsm" onclick="dlEmbedSection('${rid}',null)" style="font-size:.62rem">${IC.dna} Embed Tudo</button>`;
|
||||
h+=`</div>`;
|
||||
sectionOrder.forEach(sec=>{
|
||||
const sf=bySection[sec];
|
||||
const icon=secIcons[sec]||IC.file;
|
||||
// Section header with embed button
|
||||
h+=`<div style="display:flex;justify-content:space-between;align-items:center;margin-top:.6rem;margin-bottom:.35rem;padding-bottom:.25rem;border-bottom:1px solid var(--bd)">`;
|
||||
h+=`<div style="display:flex;align-items:center;gap:.4rem"><span style="opacity:.7">${icon}</span><span style="font-size:.72rem;font-weight:700;color:var(--t1)">${sec}</span><span style="font-size:.6rem;color:var(--t4);font-weight:400">(${sf.length})</span></div>`;
|
||||
if(hasAdb)h+=`<button class="btn bs bsm" onclick="dlEmbedSection('${rid}','${sec.replace(/'/g,"\\'")}')" style="font-size:.58rem;padding:.15rem .45rem" title="Gerar embedding desta seção">${IC.dna} Embed</button>`;
|
||||
h+=`</div>`;
|
||||
// File grid
|
||||
h+=`<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:.4rem;margin-bottom:.3rem">`;
|
||||
sf.forEach(f=>{
|
||||
const ext=f.file_name.split('.').pop().toLowerCase();
|
||||
const colors={html:'#e44d26',csv:'#217346',json:'#f5a623',xlsx:'#217346',txt:'#6b7280',pdf:'#dc2626'};
|
||||
const ac=colors[ext]||'var(--ac)';
|
||||
h+=`<a href="${API}/reports/${rid}/files/${f.id}/download?token=${S.token}" target="_blank" class="dl-card" style="display:flex;align-items:center;gap:.5rem;padding:.5rem .6rem;text-decoration:none;color:inherit;background:var(--bg);border:1px solid var(--bd);border-radius:8px;transition:all .15s;cursor:pointer" onmouseover="this.style.borderColor='${ac}';this.style.boxShadow='0 2px 8px ${ac}18'" onmouseout="this.style.borderColor='var(--bd)';this.style.boxShadow='none'">
|
||||
${_dlFileIcon(f.file_name)}
|
||||
<div style="flex:1;min-width:0;overflow:hidden"><div style="font-size:.72rem;font-weight:500;color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${f.file_name}</div></div>
|
||||
<span style="font-size:.62rem;color:var(--t4);flex-shrink:0">${_dlFmtSize(f.file_size)}</span>
|
||||
<svg viewBox="0 0 16 16" width="12" height="12" style="flex-shrink:0;opacity:.35"><path d="M8 1v9m0 0L5 7m3 3l3-3M2 12v1.5A1.5 1.5 0 003.5 15h9a1.5 1.5 0 001.5-1.5V12" stroke="var(--t3)" stroke-width="1.5" fill="none" stroke-linecap="round"/></svg></a>`});
|
||||
}
|
||||
<div style="flex:1;min-width:0;overflow:hidden"><div style="font-size:.7rem;font-weight:500;color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="${f.file_name}">${f.file_name}</div>
|
||||
<div style="font-size:.58rem;color:var(--t4);margin-top:1px">${_dlFmtSize(f.file_size)}</div></div>
|
||||
<svg viewBox="0 0 16 16" width="12" height="12" style="flex-shrink:0;opacity:.3"><path d="M8 1v9m0 0L5 7m3 3l3-3M2 12v1.5A1.5 1.5 0 003.5 15h9a1.5 1.5 0 001.5-1.5V12" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"/></svg></a>`});
|
||||
h+=`</div>`;
|
||||
});
|
||||
h+='</div>';return h}
|
||||
function dlRpt(id,f){window.open(API+'/reports/'+id+'/download?fmt='+f,'_blank')}
|
||||
async function dlEmbedSection(rid,section){
|
||||
if(!S.adbCfg.length){alert('Nenhuma conexão ADB configurada.');return}
|
||||
if(!S.genaiCfg.length&&!S.ociCfg.length){alert('Nenhuma credencial OCI configurada.');return}
|
||||
const vid=S.adbCfg.find(c=>c.is_active)?.id||S.adbCfg[0].id;
|
||||
const cfgName=S.adbCfg.find(c=>c.id===vid)?.config_name||'ADB';
|
||||
const report=S.reports.find(r=>r.id===rid);
|
||||
const reportDate=report?.created_at?new Date(report.created_at).toLocaleDateString('pt-BR'):'';
|
||||
const tenancy=report?.tenancy_name||'';
|
||||
const label=section||'todas as seções';
|
||||
const tableName=section?DL_SECTION_TABLE[section]:null;
|
||||
const tableInfo=tableName?`\nTabela: ${tableName}`:'(cada seção → tabela correspondente)';
|
||||
const purge=confirm(`Embed "${label}"\nConexão: ${cfgName}${tableInfo}\nReport: ${tenancy} — ${reportDate}\n\nDeseja LIMPAR os dados antigos antes de embedar?\n\nOK = Limpar antigos + embedar novos\nCancelar = Apenas adicionar (sem limpar)`);
|
||||
// Either way, proceed with embedding. purge=true means clean first.
|
||||
if(section&&tableName){
|
||||
try{
|
||||
if(purge)await $api('/embeddings/'+vid+'/purge',{method:'POST',body:{table_name:tableName,tenancy}});
|
||||
const d=await $api('/embeddings/report/'+rid,{method:'POST',body:{adb_config_id:vid,section,table_name:tableName}});
|
||||
alert(d.message);
|
||||
}catch(e){alert('Erro: '+e.message)}
|
||||
}else if(!section){
|
||||
const files=S.reportFiles[rid]||[];
|
||||
const sections=[...new Set(files.map(f=>_dlSection(f.file_name)))].filter(s=>DL_SECTION_TABLE[s]);
|
||||
let ok=0,fail=0;
|
||||
for(const sec of sections){
|
||||
const tbl=DL_SECTION_TABLE[sec];
|
||||
try{
|
||||
if(purge)await $api('/embeddings/'+vid+'/purge',{method:'POST',body:{table_name:tbl,tenancy}});
|
||||
await $api('/embeddings/report/'+rid,{method:'POST',body:{adb_config_id:vid,section:sec,table_name:tbl}});ok++;
|
||||
}catch(e){fail++}
|
||||
}
|
||||
alert(`Embedding: ${ok} seções enviadas${fail?' ('+fail+' erros)':''}`);
|
||||
}else{
|
||||
try{
|
||||
if(purge)await $api('/embeddings/'+vid+'/purge',{method:'POST',body:{table_name:section,tenancy}});
|
||||
const d=await $api('/embeddings/report/'+rid,{method:'POST',body:{adb_config_id:vid,section}});
|
||||
alert(d.message);
|
||||
}catch(e){alert('Erro: '+e.message)}
|
||||
}}
|
||||
function dlRpt(id,f){window.open(API+'/reports/'+id+'/download?fmt='+f+'&token='+S.token,'_blank')}
|
||||
async function refreshDl(){S.reports=await $api('/reports');S.reportFiles={};S.dlExpandedRid=null;R()}
|
||||
|
||||
/* ── Edit helpers ── */
|
||||
@@ -2315,37 +2493,52 @@ ${S.adbCfg.map(c=>{const em=S.embModels[c.embedding_model_id];const tbls=c.table
|
||||
<td>${c.username}</td><td>${tbls.length?tbls.map(t=>'<span class="tag" style="font-size:.62rem">'+t.table_name+'</span>').join(' '):'<span style="color:var(--t4);font-size:.72rem">—</span>'}</td>
|
||||
<td>${c.use_mtls?IC.ok:'—'}</td><td>${c.wallet_dir?IC.ok:IC.err}</td>
|
||||
<td>${c.genai_config_id?'<span class="tag" style="background:var(--gnl);color:var(--gn)">'+(em?em.name:c.embedding_model_id)+'</span>':'<span style="color:var(--t4)">—</span>'}</td>
|
||||
<td><button class="btn bs bsm" onclick="editCfg('adb','${c.id}')">Editar</button> <button class="btn bs bsm" onclick="tAdb('${c.id}')">Testar</button> <button class="btn bd bsm" onclick="dAdb('${c.id}')">Excluir</button></td></tr>`}).join('')}</tbody></table>
|
||||
<td><button class="btn bs bsm" onclick="editCfg('adb','${c.id}')">Editar</button> <button class="btn bs bsm" onclick="tAdb('${c.id}')">Testar</button> <button class="btn bs bsm" onclick="checkTables('${c.id}')">${IC.db} Verificar</button> <button class="btn bd bsm" onclick="dAdb('${c.id}')">Excluir</button></td></tr>`}).join('')}</tbody></table>
|
||||
${!S.adbCfg.length?'<div class="emp" style="padding:.85rem"><p>Nenhuma conexão ADB registrada.</p></div>':''}</div>
|
||||
<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>
|
||||
${ea?`<div class="cd"><div class="ct">${IC.edit} Editar Conexão — ${ea.config_name}</div><div id="am"></div>
|
||||
<div class="g2"><div class="ig"><label>Nome da Conexão</label><input type="text" id="an" placeholder="Produção ADB" value="${ea.config_name}"></div>
|
||||
<div class="ig"><label>Wallet ZIP</label><div class="ht">Envie novo wallet ou deixe vazio para manter o atual</div>
|
||||
<div style="display:flex;gap:6px;align-items:center"><input type="file" id="awf" accept=".zip" style="flex:1"><button class="btn bs bsm" id="wpbtn" type="button" onclick="parseWallet()">Analisar</button></div></div>
|
||||
<div class="ig"><label>DSN (TNS Name)</label><div class="ht">Selecione ou digite o DSN. Ex: myatp_high</div>
|
||||
<div id="adsn-wrap"><input type="text" id="adsn" placeholder="myatp_high" value="${ea?ea.dsn:''}"></div></div>
|
||||
<div class="ig"><label>Username</label><input type="text" id="auser" placeholder="ADMIN" value="${ea?ea.username:''}"></div>
|
||||
<div class="ig"><label>Password</label><input type="password" id="apw" placeholder="${ea?'(manter atual)':''}"></div>
|
||||
<div id="adsn-wrap"><input type="text" id="adsn" placeholder="myatp_high" value="${ea.dsn}"></div></div>
|
||||
<div class="ig"><label>Username</label><input type="text" id="auser" placeholder="ADMIN" value="${ea.username}"></div>
|
||||
<div class="ig"><label>Password</label><input type="password" id="apw" placeholder="(manter atual)"></div>
|
||||
<div class="ig"><label>Wallet Password</label><input type="password" id="awpw" placeholder="(opcional)"></div>
|
||||
<div class="ig"><label>Config GenAI (Embeddings)</label><div class="ht">Credenciais OCI usadas para gerar embeddings via GenAI.</div>
|
||||
<select id="agcfg"><option value="">Nenhum (sem RAG)</option>${S.genaiCfg.map(g=>'<option value="'+g.id+'"'+(ea&&ea.genai_config_id===g.id?' selected':'')+'>'+(g.name||g.model_id)+' · '+g.genai_region+'</option>').join('')}</select></div>
|
||||
<select id="agcfg"><option value="">Nenhum (sem RAG)</option>${S.genaiCfg.map(g=>'<option value="'+g.id+'"'+(ea.genai_config_id===g.id?' selected':'')+'>'+(g.name||g.model_id)+' · '+g.genai_region+'</option>').join('')}</select></div>
|
||||
<div class="ig"><label>Modelo de Embedding</label><div class="ht">Modelo OCI GenAI para gerar vetores.</div>
|
||||
<select id="aemb">${Object.entries(S.embModels).map(([k,v])=>'<option value="'+k+'"'+(ea&&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">${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>
|
||||
<select id="aemb">${Object.entries(S.embModels).map(([k,v])=>'<option value="'+k+'"'+(ea.embedding_model_id===k?' selected':'')+'>'+v.name+' ('+v.dims+'d)</option>').join('')}</select></div></div>
|
||||
<div style="display:flex;gap:8px;margin-bottom:1rem"><button class="btn bp" id="abtn" onclick="sAdb()">Salvar Alterações</button><button class="btn bs" onclick="cancelEdit()">Cancelar</button></div>
|
||||
<div style="border-top:1px solid var(--br);padding-top:.85rem">
|
||||
<div style="font-weight:600;font-size:.78rem;margin-bottom:.5rem">${IC.log} Tabelas de Vetores</div>
|
||||
<div class="cdesc" style="margin-bottom:.6rem">Registre aqui as tabelas que já existem no ADB. A busca RAG consultará todas as tabelas ativas.</div><div id="tbm"></div>
|
||||
${(ea.tables||[]).length?`<table><thead><tr><th>Tabela</th><th>Descrição</th><th>Ativa</th><th>Ações</th></tr></thead><tbody>
|
||||
${(ea.tables||[]).map(t=>`<tr${!t.is_active?' style="opacity:.55"':''}>
|
||||
<td><input type="text" value="${t.table_name}" id="tn_${t.id}" style="font-family:var(--fm);font-size:.72rem;text-transform:uppercase;width:100%;max-width:180px"></td>
|
||||
<td><input type="text" value="${t.table_name}" id="tn_${t.id}" style="font-family:var(--fm);font-size:.72rem;width:100%;max-width:180px"></td>
|
||||
<td><input type="text" value="${t.description||''}" id="td_${t.id}" placeholder="Descrição..." style="font-size:.72rem;width:100%"></td>
|
||||
<td><button class="btn bs bsm" style="font-size:.62rem" onclick="toggleTable('${ea.id}','${t.id}',${t.is_active?0:1})">${t.is_active?IC.ok+' Sim':IC.err+' Não'}</button></td>
|
||||
<td style="white-space:nowrap"><button class="btn bs bsm" onclick="saveTable('${ea.id}','${t.id}')">Salvar</button> <button class="btn bd bsm" onclick="removeTable('${ea.id}','${t.id}')">Excluir</button></td>
|
||||
</tr>`).join('')}</tbody></table>`:'<div class="emp" style="padding:.5rem"><p style="font-size:.74rem">Nenhuma tabela registrada.</p></div>'}
|
||||
<div style="display:flex;gap:6px;margin-top:.65rem;align-items:end">
|
||||
<div class="ig" style="flex:1;margin:0"><label>Nova Tabela</label><input type="text" id="newTblName" placeholder="NOME_DA_TABELA" style="text-transform:uppercase"></div>
|
||||
<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>`:''}
|
||||
<div class="ig" style="flex:1;margin:0"><label>Nome da Tabela Existente</label><input type="text" id="newTblName" placeholder="Ex: networking"></div>
|
||||
<div class="ig" style="flex:1;margin:0"><label>Descrição</label><input type="text" id="newTblDesc" placeholder="Ex: Dados de networking do CIS"></div>
|
||||
<button class="btn bs" onclick="addTable('${ea.id}')">+ Registrar</button>
|
||||
</div></div></div>`
|
||||
:`<div class="cd"><div class="ct">${IC.plus} Nova Conexão</div>
|
||||
<div class="cdesc">Conexão via <code style="font-size:.72rem">python-oracledb</code> Thin mode com Wallet (mTLS) para Oracle Autonomous Database. Envie o Wallet ZIP para preencher o DSN automaticamente.</div><div id="am"></div>
|
||||
<div class="g2"><div class="ig"><label>Nome da Conexão</label><input type="text" id="an" placeholder="Produção ADB" value=""></div>
|
||||
<div class="ig"><label>Wallet ZIP</label><div class="ht">Envie o wallet para extrair os DSNs do tnsnames.ora</div>
|
||||
<div style="display:flex;gap:6px;align-items:center"><input type="file" id="awf" accept=".zip" style="flex:1"><button class="btn bs bsm" id="wpbtn" type="button" onclick="parseWallet()">Analisar</button></div></div>
|
||||
<div class="ig"><label>DSN (TNS Name)</label><div class="ht">Selecione ou digite o DSN. Ex: myatp_high</div>
|
||||
<div id="adsn-wrap"><input type="text" id="adsn" placeholder="myatp_high" value=""></div></div>
|
||||
<div class="ig"><label>Username</label><input type="text" id="auser" placeholder="ADMIN" value=""></div>
|
||||
<div class="ig"><label>Password</label><input type="password" id="apw" placeholder=""></div>
|
||||
<div class="ig"><label>Wallet Password</label><input type="password" id="awpw" placeholder="(opcional)"></div>
|
||||
<div class="ig"><label>Config GenAI (Embeddings)</label><div class="ht">Credenciais OCI usadas para gerar embeddings via GenAI.</div>
|
||||
<select id="agcfg"><option value="">Nenhum (sem RAG)</option>${S.genaiCfg.map(g=>'<option value="'+g.id+'">'+(g.name||g.model_id)+' · '+g.genai_region+'</option>').join('')}</select></div>
|
||||
<div class="ig"><label>Modelo de Embedding</label><div class="ht">Modelo OCI GenAI para gerar vetores.</div>
|
||||
<select id="aemb">${Object.entries(S.embModels).map(([k,v])=>'<option value="'+k+'">'+v.name+' ('+v.dims+'d)</option>').join('')}</select></div></div>
|
||||
<div style="display:flex;gap:8px"><button class="btn bp" id="abtn" onclick="sAdb()">Salvar Conexão</button></div></div>`}
|
||||
${S.adbCfg.length?`<div class="cd"><div class="ct">${IC.upload} Atualizar Wallet</div><div id="wm"></div>
|
||||
<div class="g2"><div class="ig"><label>Conexão ADB</label><select id="awsel">${S.adbCfg.map(c=>`<option value="${c.id}">${c.config_name}</option>`).join('')}</select></div>
|
||||
<div class="ig"><label>Wallet ZIP</label><input type="file" id="awf2" accept=".zip"></div></div>
|
||||
@@ -2371,6 +2564,16 @@ async function sAdb(){const btn=document.getElementById('abtn');btn.disabled=tru
|
||||
await $api(url,{method,body:fd,headers:{}});S.editing=null;
|
||||
S.adbCfg=await $api('/adb/configs');sm('am',IC.ok+' Conexão '+(isEdit?'atualizada':'salva')+'!'+(wf?' Wallet incluído.':''),'s');R()}catch(e){sm('am',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Salvar Conexão'}}
|
||||
async function tAdb(id){try{const d=await $api('/adb/test/'+id,{method:'POST'});sm('am',d.status==='success'?IC.ok+' '+d.message:IC.err+' '+d.message,d.status==='success'?'s':'e');refreshCLogs('adb')}catch(e){sm('am',e.message,'e')}}
|
||||
async function checkTables(id){sm('am','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Verificando tabelas no ADB...','i');
|
||||
try{const d=await $api('/adb/'+id+'/tables/check',{method:'POST'});
|
||||
if(d.status==='error'){sm('am',IC.err+' '+d.message,'e');return}
|
||||
let html='<strong>Schema: '+d.schema_user+'</strong> ('+d.adb_tables_total+' tabelas no ADB)<br><br>';
|
||||
html+='<table style="width:100%;font-size:.74rem"><thead><tr><th>Tabela Registrada</th><th>Status</th><th>Detalhes</th></tr></thead><tbody>';
|
||||
d.registered.forEach(r=>{const st=r.status==='ok'?'<span style="color:var(--gn)">'+IC.ok+' Existe</span>':r.status==='not_found'?'<span style="color:var(--rd)">'+IC.err+' Não encontrada</span>':'<span style="color:var(--rd)">'+IC.err+' Erro</span>';
|
||||
html+='<tr><td><strong>'+r.table+'</strong></td><td>'+st+'</td><td>'+(r.rows!==undefined?r.rows+' registros':(r.message||''))+'</td></tr>'});
|
||||
html+='</tbody></table>';
|
||||
if(d.unregistered_in_adb&&d.unregistered_in_adb.length){html+='<br><details><summary style="cursor:pointer;font-size:.72rem;color:var(--t3)">Tabelas no ADB não registradas ('+d.unregistered_in_adb.length+')</summary><div style="font-size:.68rem;color:var(--t4);margin-top:.3rem">'+d.unregistered_in_adb.join(', ')+'</div></details>'}
|
||||
sm('am',html,'i')}catch(e){sm('am',IC.err+' '+e.message,'e')}}
|
||||
async function dAdb(id){if(!confirm('Excluir conexão?'))return;await $api('/adb/configs/'+id,{method:'DELETE'});S.adbCfg=await $api('/adb/configs');R()}
|
||||
async function uWallet(){const fd=new FormData();const f=document.getElementById('awf2').files[0];if(!f)return sm('wm','Selecione um arquivo wallet ZIP.','e');
|
||||
fd.append('wallet',f);const id=document.getElementById('awsel').value;if(!id)return sm('wm','Selecione uma conexão ADB.','e');
|
||||
@@ -2381,7 +2584,7 @@ async function addTable(vid){const name=document.getElementById('newTblName').va
|
||||
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',IC.ok+' Tabela atualizada.','s');R()}catch(e){sm('tbm',e.message,'e')}}
|
||||
try{await $api('/adb/'+vid+'/tables/'+tid,{method:'PUT',body:{table_name:name,description:desc}});S.adbCfg=await $api('/adb/configs');sm('tbm',IC.ok+' Tabela atualizada.','s');R()}catch(e){sm('tbm',e.message,'e')}}
|
||||
async function toggleTable(vid,tid,val){try{await $api('/adb/'+vid+'/tables/'+tid,{method:'PUT',body:{is_active:val}});S.adbCfg=await $api('/adb/configs');R()}catch(e){sm('tbm',e.message,'e')}}
|
||||
async function removeTable(vid,tid){if(!confirm('Excluir esta tabela do registro?'))return;
|
||||
try{await $api('/adb/'+vid+'/tables/'+tid,{method:'DELETE'});S.adbCfg=await $api('/adb/configs');sm('tbm',IC.ok+' Tabela removida.','s');R()}catch(e){sm('tbm',e.message,'e')}}
|
||||
@@ -2390,40 +2593,47 @@ async function removeTable(vid,tid){if(!confirm('Excluir esta tabela do registro
|
||||
function tblOpts(vid){const cfg=S.adbCfg.find(c=>c.id===vid);if(!cfg||!(cfg.tables||[]).length)return'<option value="">Padrão</option>';
|
||||
return cfg.tables.filter(t=>t.is_active).map(t=>'<option value="'+t.table_name+'">'+t.table_name+'</option>').join('')}
|
||||
function rEmbeddings(){
|
||||
const adbOpts=S.adbCfg.filter(c=>c.genai_config_id).map(c=>'<option value="'+c.id+'">'+c.config_name+'</option>').join('');
|
||||
const adbOpts=S.adbCfg.map(c=>'<option value="'+c.id+'">'+c.config_name+'</option>').join('');
|
||||
const rptOpts=S.reports.filter(r=>r.status==='completed').map(r=>'<option value="'+r.id+'">'+r.tenancy_name+' ('+r.created_at?.substring(0,10)+')</option>').join('');
|
||||
if(!S.adbCfg.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||'';
|
||||
if(!S.adbCfg.length)return`<div class="cd"><div class="emp"><div class="eic">${IC.dna}</div><p>Nenhuma conexão ADB configurada.</p><p style="font-size:.74rem;margin-top:.35rem">Configure em <strong>ADB Vector</strong>.</p></div></div>`;
|
||||
if(!S.genaiCfg.length&&!S.ociCfg.length)return`<div class="cd"><div class="emp"><div class="eic">${IC.dna}</div><p>Nenhuma credencial OCI configurada.</p><p style="font-size:.74rem;margin-top:.35rem">Configure em <strong>Config → OCI</strong>.</p></div></div>`;
|
||||
const firstVid=S.adbCfg.find(c=>c.is_active)?.id||S.adbCfg[0]?.id||'';
|
||||
return`<div class="cd"><div class="ct">${IC.log} Embeddings Existentes</div>
|
||||
<div class="cdesc">Consulte os documentos armazenados nas tabelas de embeddings do ADB.</div>
|
||||
<div class="g3"><div class="ig"><label>Conexão ADB</label><select id="elsel" onchange="document.getElementById('eltbl').innerHTML=tblOpts(this.value)">${adbOpts}</select></div>
|
||||
<div class="ig"><label>Tabela</label><select id="eltbl">${tblOpts(firstVid)}</select></div>
|
||||
<div class="ig" style="align-self:end"><button class="btn bs" onclick="loadEmbs()">Carregar</button></div></div>
|
||||
<div id="emb-list"></div></div>
|
||||
<div class="cd"><div class="ct">${IC.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()">${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">${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>
|
||||
<div class="ig"><label>Arquivo (.txt)</label><input type="file" id="eff" accept=".txt"></div></div>
|
||||
<button class="btn bp" id="efbtn" onclick="embedFile()">Upload e Gerar Embeddings</button></div>`}
|
||||
<div class="cd"><div class="ct">${IC.shield} CIS Recommendations</div>
|
||||
<div class="cdesc">Envie a versão mais recente do PDF para manter as recomendações CIS Oracle Cloud atualizadas na base vetorial.</div><div id="crm"></div>
|
||||
<div class="g3"><div class="ig"><label>Conexão ADB</label><select id="crsel">${adbOpts}</select></div>
|
||||
<div class="ig"><label>Arquivo PDF</label><input type="file" id="crf" accept=".pdf,.txt"></div>
|
||||
<div class="ig" style="align-self:end"><button class="btn bp" id="crbtn" onclick="embedCisRecom()">${IC.upload} Upload CIS Recommendations</button></div></div></div>
|
||||
<div class="cd"><div class="ct">${IC.scroll} Base de Conhecimento</div>
|
||||
<div class="cdesc">Envie documentos para alimentar a base de conhecimento da equipe. Os arquivos serão vetorizados e disponibilizados para consulta via chat.</div><div id="efm"></div>
|
||||
<div class="g3"><div class="ig"><label>Conexão ADB</label><select id="efsel">${adbOpts}</select></div></div>
|
||||
<div style="display:flex;gap:.75rem;align-items:start;flex-wrap:wrap;margin-top:.5rem">
|
||||
<div style="flex:1;min-width:240px">
|
||||
<label style="font-size:.72rem;font-weight:600;display:block;margin-bottom:.35rem">Arquivo (.txt, .pdf, .csv, .json, .md)</label>
|
||||
<input type="file" id="eff" accept=".txt,.pdf,.csv,.json,.md">
|
||||
<button class="btn bp" id="efbtn" onclick="embedFile()" style="margin-top:.5rem;width:100%">Upload Arquivo</button>
|
||||
</div>
|
||||
<div style="width:1px;background:var(--brd);align-self:stretch;margin:0 .25rem"></div>
|
||||
<div style="flex:1;min-width:240px">
|
||||
<label style="font-size:.72rem;font-weight:600;display:block;margin-bottom:.35rem">URL (página web ou PDF)</label>
|
||||
<input type="text" id="efurl" placeholder="https://docs.oracle.com/..." style="width:100%;padding:.45rem .6rem;border:1px solid var(--brd);border-radius:var(--r);background:var(--bg2);color:var(--t1);font-size:.78rem">
|
||||
<button class="btn bp" id="efurlbtn" onclick="embedUrl()" style="margin-top:.5rem;width:100%">Importar URL</button>
|
||||
</div></div></div>`}
|
||||
|
||||
async function loadEmbs(){const vid=document.getElementById('elsel').value;if(!vid)return;
|
||||
const tbl=document.getElementById('eltbl')?.value||'';
|
||||
try{const d=await $api('/embeddings/'+vid+'/list'+(tbl?'?table_name='+encodeURIComponent(tbl):''));
|
||||
if(!d.documents.length){document.getElementById('emb-list').innerHTML='<div class="emp" style="padding:.65rem"><p>Nenhum embedding encontrado.</p></div>';return}
|
||||
document.getElementById('emb-list').innerHTML='<table style="margin-top:.65rem"><thead><tr><th>ID</th><th>Source</th><th>Metadata</th><th>Data</th><th>Ações</th></tr></thead><tbody>'+
|
||||
d.documents.map(e=>'<tr><td style="font-family:var(--fm);font-size:.62rem">'+e.id.substring(0,8)+'...</td><td>'+
|
||||
(e.source||'—')+'</td><td style="font-size:.72rem;color:var(--t3)">'+(e.metadata||'—')+'</td><td style="font-size:.72rem;color:var(--t4)">'+
|
||||
(e.created_at||'—')+'</td><td><button class="btn bd bsm" onclick="delEmb(\''+vid+'\',\''+e.id+'\')">Excluir</button></td></tr>').join('')+
|
||||
'</tbody></table><div style="padding:.5rem;font-size:.72rem;color:var(--t4)">Total: '+d.total+' documentos</div>'}
|
||||
document.getElementById('emb-list').innerHTML='<div style="overflow-x:auto"><table style="margin-top:.65rem;table-layout:fixed;width:100%"><thead><tr><th style="width:80px">ID</th><th>Metadata</th></tr></thead><tbody>'+
|
||||
d.documents.map(e=>{let meta=e.metadata||'—';if(typeof meta==='object')meta=JSON.stringify(meta);if(meta.length>200)meta=meta.substring(0,200)+'…';
|
||||
return'<tr><td style="font-family:var(--fm);font-size:.62rem;word-break:break-all">'+e.id.substring(0,12)+'</td>'+
|
||||
'<td style="font-size:.68rem;color:var(--t3);word-break:break-word;white-space:pre-wrap;max-width:0;overflow:hidden">'+meta+'</td></tr>'}).join('')+
|
||||
'</tbody></table></div><div style="padding:.5rem;font-size:.72rem;color:var(--t4)">Total: '+d.total+' documentos</div>'}
|
||||
catch(e){document.getElementById('emb-list').innerHTML='<div style="color:var(--rd);padding:.5rem">Erro: '+e.message+'</div>'}}
|
||||
|
||||
async function previewChunks(){const rid=document.getElementById('errpt').value;
|
||||
@@ -2455,12 +2665,22 @@ async function embedReport(){const vid=document.getElementById('ersel').value;co
|
||||
const btn=document.getElementById('erbtn');btn.disabled=true;btn.textContent='Gerando...';sm('erm','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Gerando embeddings do relatório...','i');
|
||||
try{const d=await $api('/embeddings/report/'+rid,{method:'POST',body:{adb_config_id:vid,table_name:tbl||undefined}});sm('erm',IC.ok+' '+d.message,'s');btn.disabled=false;btn.textContent='Gerar Embeddings do Relatório'}catch(e){sm('erm',e.message,'e');btn.disabled=false;btn.textContent='Gerar Embeddings do Relatório'}}
|
||||
|
||||
async function embedCisRecom(){const vid=document.getElementById('crsel').value;const f=document.getElementById('crf').files[0];
|
||||
if(!vid||!f)return sm('crm','Selecione conexão ADB e arquivo.','e');
|
||||
const fd=new FormData();fd.append('file',f);fd.append('adb_config_id',vid);fd.append('table_name','cisrecom');
|
||||
const btn=document.getElementById('crbtn');btn.disabled=true;btn.textContent='Enviando...';sm('crm','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Processando PDF e gerando embeddings...','i');
|
||||
try{const d=await $api('/embeddings/upload',{method:'POST',body:fd,headers:{}});sm('crm',IC.ok+' '+d.message+' → tabela CISRECOM','s');btn.disabled=false;btn.textContent=IC.upload+' Upload CIS Recommendations'}catch(e){sm('crm',e.message,'e');btn.disabled=false;btn.textContent=IC.upload+' Upload CIS Recommendations'}}
|
||||
async function embedFile(){const vid=document.getElementById('efsel').value;const f=document.getElementById('eff').files[0];
|
||||
if(!vid||!f)return sm('efm','Selecione conexão ADB e arquivo.','e');
|
||||
const tbl=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 tbl='engineerknowledgebase';
|
||||
const fd=new FormData();fd.append('file',f);fd.append('adb_config_id',vid);fd.append('table_name',tbl);
|
||||
const btn=document.getElementById('efbtn');btn.disabled=true;btn.textContent='Enviando...';sm('efm','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Enviando e gerando embeddings...','i');
|
||||
try{const d=await $api('/embeddings/upload',{method:'POST',body:fd,headers:{}});sm('efm',IC.ok+' '+d.message,'s');btn.disabled=false;btn.textContent='Upload 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 Arquivo'}catch(e){sm('efm',e.message,'e');btn.disabled=false;btn.textContent='Upload Arquivo'}}
|
||||
async function embedUrl(){const vid=document.getElementById('efsel').value;const url=document.getElementById('efurl').value.trim();
|
||||
if(!vid||!url)return sm('efm','Selecione conexão ADB e informe a URL.','e');
|
||||
const fd=new FormData();fd.append('adb_config_id',vid);fd.append('table_name','engineerknowledgebase');fd.append('url',url);
|
||||
const btn=document.getElementById('efurlbtn');btn.disabled=true;btn.textContent='Importando...';sm('efm','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Buscando conteúdo e gerando embeddings...','i');
|
||||
try{const d=await $api('/embeddings/upload-url',{method:'POST',body:fd,headers:{}});sm('efm',IC.ok+' '+d.message,'s');btn.disabled=false;btn.textContent='Importar URL';document.getElementById('efurl').value=''}catch(e){sm('efm',e.message,'e');btn.disabled=false;btn.textContent='Importar URL'}}
|
||||
|
||||
async function delEmb(vid,docId){if(!confirm('Excluir este embedding?'))return;
|
||||
const tbl=document.getElementById('eltbl')?.value||'';
|
||||
|
||||
Reference in New Issue
Block a user