From c4a9f8d6d56694b6ebe3b452ee53f3f84ae61602 Mon Sep 17 00:00:00 2001 From: nogueiraguh Date: Mon, 9 Mar 2026 15:01:26 -0300 Subject: [PATCH] feat: ADB network access, prompt editor, resize persistence, and multiple fixes - Add Update Network Access endpoint and UI for ADB ACL management - Add inline prompt editor with auto-save in Terraform agent menu - Persist resize bar positions across re-renders (Explorer + Terraform) - Restore full Terraform session state from history (files, plan, outputs, OCI config) - Parse free-form Resource Plan format (model often skips ```plan blocks) - Auto-refresh resource state after Start/Stop actions - Clear previous apply/destroy outputs on Re-plan - Deduplicate CIS Compliance Scanner MCP server on startup - Show ACL IPs in Explorer for Autonomous Databases - Detect user IP via /api/oci/my-ip endpoint --- backend/app.py | 66 +++++++++++++++---- frontend/index.html | 150 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 188 insertions(+), 28 deletions(-) diff --git a/backend/app.py b/backend/app.py index 948e8b6..6823871 100644 --- a/backend/app.py +++ b/backend/app.py @@ -874,7 +874,7 @@ async def explore_databases(cid: str, compartment_id: str = Query(None), region: adbs = db_client.list_autonomous_databases(comp).data return [{"id":a.id,"display_name":a.display_name,"db_name":a.db_name,"lifecycle_state":a.lifecycle_state, "cpu_core_count":a.cpu_core_count,"data_storage_size_in_tbs":a.data_storage_size_in_tbs, - "is_free_tier":a.is_free_tier} for a in adbs] + "is_free_tier":a.is_free_tier,"whitelisted_ips":a.whitelisted_ips or []} for a in adbs] except Exception as e: return {"error": str(e)[:500]} @@ -3646,6 +3646,43 @@ async def oci_adb_action(adb_id: str, req: dict, u=Depends(current_user)): raise HTTPException(500, str(e)[:500]) +@app.post("/api/oci/autonomous-databases/{adb_id}/update-network-access") +async def oci_adb_update_network(adb_id: str, req: dict, u=Depends(current_user)): + oci_config_id = req.get("oci_config_id", "") + ip = req.get("ip", "") + region = req.get("region") + if not oci_config_id: + raise HTTPException(400, "oci_config_id obrigatório") + if not ip: + raise HTTPException(400, "ip obrigatório") + try: + import oci + config = _get_oci_config(oci_config_id) + if region: + config["region"] = region + db_client = oci.database.DatabaseClient(config) + adb = db_client.get_autonomous_database(adb_id).data + current_acl = adb.whitelisted_ips or [] + # Build new ACL: keep existing entries, add new IP if not present + ip_cidr = ip if "/" in ip else ip + "/32" + new_acl = list(set(current_acl) | {ip_cidr}) + db_client.update_autonomous_database( + adb_id, + oci.database.models.UpdateAutonomousDatabaseDetails(whitelisted_ips=new_acl)) + _audit(u["id"], u["username"], "adb_update_network", adb_id, f"ip={ip_cidr}") + return {"ok": True, "adb_id": adb_id, "ip_added": ip_cidr, "acl": new_acl} + except Exception as e: + raise HTTPException(500, str(e)[:500]) + + +@app.get("/api/oci/my-ip") +async def get_my_ip(request: Request): + """Return the caller's public IP address.""" + forwarded = request.headers.get("x-forwarded-for", "") + ip = forwarded.split(",")[0].strip() if forwarded else (request.client.host if request.client else "") + return {"ip": ip} + + @app.get("/api/terraform/resources") async def tf_list_resources(oci_config_id: str = Query(...), compartment_id: str = Query(...), region: str = Query(None), u=Depends(current_user)): """List existing OCI resources in a compartment for terraform context.""" @@ -3720,7 +3757,7 @@ async def tf_run_plan(wid: str, bg: BackgroundTasks, u=Depends(current_user)): if ws["status"] in ("planning", "applying", "destroying"): raise HTTPException(400, f"Workspace is {ws['status']}") with db() as c: - c.execute("UPDATE terraform_workspaces SET status='planning', plan_output='', error=NULL, updated_at=datetime('now') WHERE id=?", (wid,)) + c.execute("UPDATE terraform_workspaces SET status='planning', plan_output='', apply_output='', destroy_output='', error=NULL, updated_at=datetime('now') WHERE id=?", (wid,)) bg.add_task(_terraform_exec, wid, "plan", dict(u)) return {"status": "planning"} @@ -4178,16 +4215,18 @@ provider "oci" {{ _running_terraform.pop(wid, None) if proc.returncode == 0: - _update_output(f"\n✅ terraform {action} completed successfully") + output_lines.append(f"\n✅ terraform {action} completed successfully") + full_output = "\n".join(output_lines) with db() as c: - c.execute("UPDATE terraform_workspaces SET status=?, updated_at=datetime('now') WHERE id=?", - (final_ok, wid)) + c.execute(f"UPDATE terraform_workspaces SET {status_col}=?, status=?, updated_at=datetime('now') WHERE id=?", + (full_output, final_ok, wid)) _audit(user["id"], user["username"], f"terraform_{action}", wid, f"status={final_ok}") else: - _update_output(f"\n❌ terraform {action} failed (exit {proc.returncode})") + output_lines.append(f"\n❌ terraform {action} failed (exit {proc.returncode})") + full_output = "\n".join(output_lines) with db() as c: - c.execute("UPDATE terraform_workspaces SET status='failed', error=?, updated_at=datetime('now') WHERE id=?", - (f"terraform {action} failed (exit {proc.returncode})", wid)) + c.execute(f"UPDATE terraform_workspaces SET {status_col}=?, status='failed', error=?, updated_at=datetime('now') WHERE id=?", + (full_output, f"terraform {action} failed (exit {proc.returncode})", wid)) except Exception as e: log.error(f"Terraform exec error: {e}") @@ -4376,10 +4415,15 @@ async def startup(): orphaned = c.execute("UPDATE reports SET status='failed', error_msg='Interrompido: container reiniciado' WHERE status='running'").rowcount if orphaned: log.warning(f"Marked {orphaned} orphaned running report(s) as failed") - # Auto-register CIS Compliance Scanner MCP server if not present + # Auto-register CIS Compliance Scanner MCP server if not present (deduplicate on startup) with db() as c: - existing = c.execute("SELECT id FROM mcp_servers WHERE name='CIS Compliance Scanner'").fetchone() - if not existing: + dupes = c.execute("SELECT id FROM mcp_servers WHERE name='CIS Compliance Scanner' ORDER BY created_at ASC").fetchall() + if len(dupes) > 1: + keep = dupes[0]["id"] + for d in dupes[1:]: + c.execute("DELETE FROM mcp_servers WHERE id=?", (d["id"],)) + log.info(f"Removed {len(dupes)-1} duplicate CIS Compliance Scanner entries, kept {keep}") + if not dupes: mid = str(uuid.uuid4()) c.execute( "INSERT INTO mcp_servers (id, user_id, name, description, server_type, command, args) VALUES (?,?,?,?,?,?,?)", diff --git a/frontend/index.html b/frontend/index.html index d0d28fd..eecb398 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -328,6 +328,12 @@ tbody tr:last-child td{border-bottom:none} .tf-menu-item.loading{opacity:.6;pointer-events:none} .tf-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:100} .tf-modal{background:var(--bg);border-radius:var(--r);padding:1.5rem;max-width:420px;width:90%;box-shadow:0 8px 30px rgba(0,0,0,.25)} +.tf-modal.wide{max-width:720px} +.tf-prompt-ta{width:100%;min-height:320px;max-height:60vh;background:var(--bg2);color:var(--t1);border:1px solid var(--bd);border-radius:6px;padding:.6rem;font-family:monospace;font-size:.72rem;line-height:1.5;resize:vertical;box-sizing:border-box} +.tf-prompt-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:.6rem} +.tf-prompt-header h3{margin:0;color:var(--ac);font-size:.82rem} +.tf-prompt-saved{font-size:.62rem;color:var(--t3);transition:opacity .3s} +.tf-prompt-name{background:var(--bg2);color:var(--t1);border:1px solid var(--bd);border-radius:4px;padding:.25rem .4rem;font-size:.72rem;width:100%;margin-bottom:.5rem;box-sizing:border-box} .tf-modal h3{margin:0 0 .5rem;color:var(--rd)} .tf-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--t3);font-size:.76rem;gap:.3rem} .tf-empty svg{width:40px;height:40px;fill:#7b42bc;opacity:.3} @@ -343,7 +349,7 @@ const V='1.1'; const LOGO_W=``; const LOGO_R=``; -const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],models:{},regions:[],embModels:{},expData:null,expCfg:'',expSelRegions:[],expRegDdOpen:false,expTree:[],expSelComp:'',expCat:'Compute',expResType:'instances',expTreeOpen:{},expLoading:false,expRegions:[],expCounts:{},editing:null, +const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],models:{},regions:[],embModels:{},expData:null,expCfg:'',expSelRegions:[],expRegDdOpen:false,expTree:[],expSelComp:'',expCat:'Compute',expResType:'instances',expTreeOpen:{},expLoading:false,expRegions:[],expCounts:{},expTreeW:null,editing:null, chatModel:'',chatOci:'',chatRegion:'',chatCompartment:'',chatHistOpen:false,chatHistory:[],tfHistOpen:false,tfHistory:[], chatParams:{temperature:1,max_tokens:6000,top_p:0.95,top_k:1,frequency_penalty:0,presence_penalty:0},chatPanel:'',chatUseTools:true, chatPrompts:[],editingPrompt:null,trackingReportId:null,ociRegions:{},rptSelRegions:[],rptRegionsOpen:false,rptRegionFilter:'', @@ -352,7 +358,7 @@ const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],g cisVer:null,cisCheckResult:null,cisUpdating:false,rptHistFilter:'',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:''}; + tfComps:[],tfCompLoading:false,tfFiles:[],tfBtab:'files',tfEditIdx:-1,tfResources:null,tfResLoading:false,tfRefStatus:'',tfBottomH:null}; const API='/api'; async function $api(p,o={}){const h={...(o.headers||{})};if(S.token)h['Authorization']='Bearer '+S.token; @@ -404,6 +410,8 @@ function R(){try{ // Restore scroll positions requestAnimationFrame(()=>{ Object.entries(scrollMap).forEach(([id,top])=>{if(id==='_bcLeft'){const el=document.querySelector('.tf-bc-left');if(el)el.scrollTop=top}else{const el=document.getElementById(id);if(el)el.scrollTop=top}}); + if(S.tfBottomH){const bel=document.getElementById('tfBottom');if(bel){bel.style.height=S.tfBottomH+'px';bel.style.flex='none'}} + if(S.expTreeW){const tel=document.getElementById('exp-tree');if(tel)tel.style.width=S.expTreeW+'px'} }); if(S.editing?.type==='mcp')setTimeout(mcpTypeFields,0) }catch(e){console.error('Render error:',e);document.getElementById('app').innerHTML='

Render Error

'+e.message+'
'}} @@ -613,6 +621,33 @@ function tfToggleMenu(){ m.style.display=show?'block':'none'; if(show){const h=e=>{if(!m.contains(e.target)&&!e.target.closest('[onclick*="tfToggleMenu"]')){m.style.display='none';document.removeEventListener('click',h)}};setTimeout(()=>document.addEventListener('click',h),0)} } +let _tfPromptSaveTimer=null; +async function tfOpenPrompt(){ + document.getElementById('tfMenu').style.display='none'; + try{ + const prompts=await $api('/prompts/terraform'); + const active=prompts.find(p=>p.is_active)||prompts[0]; + if(!active){alert('Nenhum prompt encontrado.');return} + S.tfPrompt={id:active.id,name:active.name,content:active.content}; + S.tfPromptOpen=true;R(); + }catch(e){alert('Erro ao carregar prompt: '+e.message)} +} +function tfPromptChanged(field,value){ + if(!S.tfPrompt)return; + S.tfPrompt[field]=value; + const el=document.getElementById('tfPromptSaved'); + if(el){el.textContent='Salvando...';el.style.opacity='1'} + clearTimeout(_tfPromptSaveTimer); + _tfPromptSaveTimer=setTimeout(async()=>{ + try{ + await $api('/prompts/'+S.tfPrompt.id,{method:'PUT',body:{name:S.tfPrompt.name,content:S.tfPrompt.content,is_active:true}}); + const el=document.getElementById('tfPromptSaved'); + if(el){el.textContent='Salvo ✓';el.style.opacity='1';setTimeout(()=>{if(el)el.style.opacity='.4'},2000)} + }catch(e){const el=document.getElementById('tfPromptSaved');if(el){el.textContent='Erro ao salvar';el.style.opacity='1'}} + },800); +} +function tfClosePrompt(){S.tfPromptOpen=false;R()} + async function tfRefreshRef(){ const item=document.querySelector('.tf-menu-item'); const sub=document.getElementById('tfRefStatus'); @@ -644,11 +679,44 @@ async function loadSession(sid,type){ S.sid=sid;S.msgs=d.messages.map(m=>({r:m.role,c:m.content,t:m.created_at?.slice(11,16)||''}));R();scCh(); }else{ S.tfSid=sid;S.tfMsgs=d.messages.map(m=>({r:m.role,c:m.content,t:m.created_at?.slice(11,16)||''})); - S.tfFiles=[];S.tfCode='';S.tfPlan=[]; - const last=[...d.messages].reverse().find(m=>m.role==='assistant'&&m.content&&m.content.includes('```')); - if(last)fmTf(last.content); + S.tfFiles=[];S.tfCode='';S.tfPlan=[];S.tfPlanOut='';S.tfApplyOut='';S.tfDestroyOut=''; try{const wsList=await $api('/terraform/workspaces');const ws=wsList.find(w=>w.session_id===sid); - if(ws){S.tfWs=ws.id;S.tfStatus=ws.status}else{S.tfWs=null;S.tfStatus='draft'} + if(ws){ + S.tfWs=ws.id;S.tfStatus=ws.status; + const detail=await $api('/terraform/workspaces/'+ws.id); + S.tfPlanOut=detail.plan_output||'';S.tfApplyOut=detail.apply_output||'';S.tfDestroyOut=detail.destroy_output||''; + if(detail.tf_code){ + S.tfCode=detail.tf_code; + const parts=detail.tf_code.split(/^\/\/\s*filename:\s*/m).filter(Boolean); + let md=''; + if(parts.length>1){ + md=parts.map(p=>{const nl=p.indexOf('\n');const fn=p.substring(0,nl).trim();const code=p.substring(nl+1).trim();return '```hcl\n// filename: '+fn+'\n'+code+'\n```'}).join('\n\n'); + }else{md='```hcl\n'+detail.tf_code+'\n```'} + const lastMsg=[...d.messages].reverse().find(m=>m.role==='assistant'&&m.content&&(m.content.includes('```plan')||/Resource Plan/.test(m.content))); + if(lastMsg){ + const pm=lastMsg.content.match(/```plan\s*\n([\s\S]*?)```/); + if(pm){md+='\n\n```plan\n'+pm[1]+'```'} + else{ + const fm=lastMsg.content.match(/(\*{0,2}Resource Plan\*{0,2})\s*\n((?:[+~][\s\S]*?)(?=\n\n|\n##|\n\*\*|$))/); + if(fm)md+='\n\n'+fm[0]; + } + } + fmTf(md); + } + if(detail.oci_config_id){ + S.tfOci=detail.oci_config_id; + const oc=S.ociCfg.find(x=>x.id===detail.oci_config_id); + if(oc)S.tfRegion=oc.region; + } + if(detail.compartment_id)S.tfCompartment=detail.compartment_id; + if(S.tfPlanOut)S.tfBtab='plan'; + else if(S.tfFiles.length)S.tfBtab='files'; + if(S.tfOci){tfLoadComps();if(S.tfCompartment)tfLoadResources()} + }else{ + S.tfWs=null;S.tfStatus='draft'; + const last=[...d.messages].reverse().find(m=>m.role==='assistant'&&m.content&&m.content.includes('```')); + if(last)fmTf(last.content); + } }catch(e){S.tfWs=null;S.tfStatus='draft'} R(); } @@ -847,9 +915,20 @@ function fmTf(t){ r=r.replace(/```plan\s*\n([\s\S]*?)```/g,(m,plan)=>{ const lines=plan.trim().split('\n'); S.tfPlan=lines.map(l=>{const m2=l.match(/^[+~]\s+(\S+)\s+\((.+)\)$/);return m2?{type:m2[1],desc:m2[2]}:{type:l.replace(/^[+~]\s*/,'').trim(),desc:''}}).filter(x=>x.type); - return '
'+TF_ICON+' Resource Plan — '+S.tfPlan.length+' resource(s)
'+ - S.tfPlan.map(x=>'
+'+escHtml(x.type)+''+escHtml(x.desc)+'
').join('')+'
'; + return _renderTfPlanBlock(); }); + // Also match free-form "Resource Plan" / "**Resource Plan**" followed by +/~ lines (model often skips ```plan block) + if(!S.tfPlan.length){ + r=r.replace(/(\*{0,2}Resource Plan\*{0,2})\s*\n((?:[+~][\s\S]*?)(?=\n\n|\n##|\n\*\*|$))/g,(m,hdr,plan)=>{ + const lines=plan.trim().split('\n').filter(l=>/^[+~]/.test(l.trim())); + S.tfPlan=lines.map(l=>{const clean=l.trim().replace(/^[+~]\s*/,'').replace(/\s{2,}/g,' ').trim();return{type:clean,desc:''}}).filter(x=>x.type); + return _renderTfPlanBlock(); + }); + } + function _renderTfPlanBlock(){ + return '
'+TF_ICON+' Resource Plan — '+S.tfPlan.length+' resource(s)
'+ + S.tfPlan.map(x=>'
+'+escHtml(x.type)+''+(x.desc?''+escHtml(x.desc)+'':'')+'
').join('')+'
'; + } // Auto-split: if model returned 1-2 large blocks, split into multiple files // Runs on BOTH first generation and corrections — monolithic responses always get split let didAutoSplit=false; @@ -945,6 +1024,10 @@ function rTerraform(){
${S.tfConfirmDest?rTfConfirmDestroy():''} + ${S.tfPromptOpen?rTfPromptModal():''}
`; } +function rTfPromptModal(){ + const p=S.tfPrompt||{}; + return`
+

Editar Prompt — Terraform Agent

Salvo ✓
+ + +
+
`; +} function _tfBadge(st){ const m={draft:['draft','Draft'],planning:['run','Planning...'],planned:['ok','Plan OK'],applying:['run','Applying...'],applied:['ok','Applied'],destroying:['run','Destroying...'],destroyed:['dest','Destroyed'],failed:['err','Failed']}; @@ -1089,7 +1182,7 @@ function tfStartResize(e){ e.preventDefault();const el=document.getElementById('tfBottom');if(!el)return; const startY=e.clientY;const startH=el.offsetHeight; const bar=e.target;bar.classList.add('dragging'); - function onMove(ev){const dh=startY-ev.clientY;el.style.height=Math.max(100,Math.min(startH+dh,window.innerHeight-200))+'px'} + function onMove(ev){const dh=startY-ev.clientY;const h=Math.max(100,Math.min(startH+dh,window.innerHeight-200));el.style.height=h+'px';el.style.flex='none';S.tfBottomH=h} function onUp(){bar.classList.remove('dragging');document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp)} document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp); } @@ -1179,12 +1272,34 @@ function rTfResourcesPanel(){ async function ociInstanceAction(id,action){ const label=action==='START'?'Iniciar':'Parar'; if(!confirm(label+' esta instância?'))return; - try{await $api('/oci/instances/'+id+'/action',{method:'POST',body:{action,oci_config_id:S.expCfg,region:null}});alert('Instância '+(action==='START'?'iniciando':'parando')+'...');expLoadRes()}catch(e){alert('Erro: '+e.message)} + try{await $api('/oci/instances/'+id+'/action',{method:'POST',body:{action,oci_config_id:S.expCfg,region:null}});expPollState(action==='START'?'RUNNING':'STOPPED')}catch(e){alert('Erro: '+e.message)} } async function ociAdbAction(id,action){ const label=action==='start'?'Iniciar':'Parar'; if(!confirm(label+' este Autonomous Database?'))return; - try{await $api('/oci/autonomous-databases/'+id+'/action',{method:'POST',body:{action,oci_config_id:S.expCfg,region:null}});alert('Autonomous DB '+(action==='start'?'iniciando':'parando')+'...');expLoadRes()}catch(e){alert('Erro: '+e.message)} + try{await $api('/oci/autonomous-databases/'+id+'/action',{method:'POST',body:{action,oci_config_id:S.expCfg,region:null}});expPollState(action==='start'?'AVAILABLE':'STOPPED')}catch(e){alert('Erro: '+e.message)} +} +async function ociAdbUpdateNet(id){ + try{ + const ipResp=await $api('/oci/my-ip'); + let ip=ipResp.ip||''; + ip=prompt('Adicionar IP à ACL do Autonomous Database.\nSeu IP detectado:',ip); + if(!ip)return; + await $api('/oci/autonomous-databases/'+id+'/update-network-access',{method:'POST',body:{ip,oci_config_id:S.expCfg}}); + alert('IP '+ip+' adicionado à ACL com sucesso.'); + expLoadRes(); + }catch(e){alert('Erro: '+e.message)} +} +function expPollState(target){ + let tries=0;const max=12; + const poll=async()=>{ + await expLoadRes(); + const items=S.expData||[]; + const reached=items.some(i=>i.lifecycle_state===target); + if(reached||++tries>=max)return; + setTimeout(poll,5000); + }; + setTimeout(poll,3000); } function tfSwitchBtab(tab){ S.tfBtab=tab;S.tfEditIdx=-1; @@ -1265,7 +1380,7 @@ async function tfSaveAndPlan(){ } // Run plan await $api('/terraform/workspaces/'+S.tfWs+'/plan',{method:'POST'}); - S.tfStatus='planning';S.tfRunning=true;S.tfPlanOut='';S.tfBtab='plan';R(); + S.tfStatus='planning';S.tfRunning=true;S.tfPlanOut='';S.tfApplyOut='';S.tfDestroyOut='';S.tfBtab='plan';R(); await tfPollExec(); }catch(e){alert('Erro: '+e.message)} } @@ -1393,7 +1508,7 @@ function expSetTab(t){S.expResType=t;S.expData=null;R();if(S.expSelComp)expLoadR function expToggleNode(id){S.expTreeOpen[id]=!S.expTreeOpen[id];R()} async function expSelectComp(id){S.expSelComp=id;S.expCounts={};await expLoadRes()} function expStartResize(e){e.preventDefault();const tree=document.getElementById('exp-tree');const handle=e.target;if(!tree)return;handle.classList.add('dragging');const startX=e.clientX;const startW=tree.offsetWidth; - const onMove=ev=>{const w=Math.max(140,Math.min(startW+(ev.clientX-startX),window.innerWidth*0.5));tree.style.width=w+'px'}; + const onMove=ev=>{const w=Math.max(140,Math.min(startW+(ev.clientX-startX),window.innerWidth*0.5));tree.style.width=w+'px';S.expTreeW=w}; const onUp=()=>{handle.classList.remove('dragging');document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);document.body.style.cursor='';document.body.style.userSelect=''}; document.body.style.cursor='col-resize';document.body.style.userSelect='none';document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp)} async function expRefresh(){if(S.expCfg){S.expTree=[];S.expCounts={};R();await expLoadTree();if(S.expSelComp)await expLoadRes()}} @@ -1437,13 +1552,14 @@ function renderExpData(d){if(!d)return'

Selecione um compartm return`

${d.length} item(ns) encontrado(s)
${d.map(i=>{ const keys=Object.keys(i); const name=i.display_name||i.name||i.db_name||i.application||'—'; - const skip=['display_name','name','db_name','lifecycle_state','application','rules','routes']; + const skip=['display_name','name','db_name','lifecycle_state','application','rules','routes','whitelisted_ips']; + const aclHtml=isAdb&&i.whitelisted_ips&&i.whitelisted_ips.length?'
ACL: '+i.whitelisted_ips.join(', ')+'':''; 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=' '; - return`
${name}${i.lifecycle_state?` ${i.lifecycle_state}`:''}${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){ return`
${d.length} security list(s)
${d.map((sl,idx)=>{