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
This commit is contained in:
@@ -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 (?,?,?,?,?,?,?)",
|
||||
|
||||
@@ -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=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="26" height="26" style="flex-shrink:0"><rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(255,255,255,0.12)" stroke="#fff" stroke-width="2"/><rect x="11" y="11" width="14" height="14" rx="4" fill="#fff" opacity="0.95"/><circle cx="15" cy="17" r="2" fill="#C74634"/><circle cx="21" cy="17" r="2" fill="#C74634"/><circle cx="15" cy="16.7" r="0.7" fill="#fff"/><circle cx="21" cy="16.7" r="0.7" fill="#fff"/><rect x="14" y="21" width="8" height="1.5" rx="0.75" fill="#C74634" opacity="0.6"/><line x1="18" y1="11" x2="18" y2="6" stroke="#fff" stroke-width="1.2" stroke-linecap="round"/><circle cx="18" cy="5.5" r="1.5" fill="#fff" opacity="0.9"/><line x1="11" y1="18" x2="6" y2="18" stroke="#fff" stroke-width="1" stroke-linecap="round" opacity="0.7"/><line x1="25" y1="18" x2="30" y2="18" stroke="#fff" stroke-width="1" stroke-linecap="round" opacity="0.7"/><circle cx="5.5" cy="18" r="1" fill="#fff" opacity="0.7"/><circle cx="30.5" cy="18" r="1" fill="#fff" opacity="0.7"/></svg>`;
|
||||
const LOGO_R=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="52" height="52"><rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(199,70,52,0.08)" stroke="#C74634" stroke-width="1.8"/><rect x="11" y="11" width="14" height="14" rx="4" fill="#fff" stroke="#C74634" stroke-width="0.4"/><circle cx="15" cy="17" r="2" fill="#C74634"/><circle cx="21" cy="17" r="2" fill="#C74634"/><circle cx="15" cy="16.7" r="0.7" fill="#fff"/><circle cx="21" cy="16.7" r="0.7" fill="#fff"/><rect x="14" y="21" width="8" height="1.5" rx="0.75" fill="#C74634" opacity="0.6"/><line x1="18" y1="11" x2="18" y2="6" stroke="#C74634" stroke-width="1.2" stroke-linecap="round"/><circle cx="18" cy="5.5" r="1.5" fill="#C74634" opacity="0.25" stroke="#C74634" stroke-width="0.7"/><line x1="11" y1="18" x2="6" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.4"/><line x1="25" y1="18" x2="30" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.4"/><circle cx="5.5" cy="18" r="1" fill="#C74634" opacity="0.35"/><circle cx="30.5" cy="18" r="1" fill="#C74634" opacity="0.35"/></svg>`;
|
||||
|
||||
const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],models:{},regions:[],embModels:{},expData:null,expCfg:'',expSelRegions:[],expRegDdOpen:false,expTree:[],expSelComp:'',expCat:'Compute',expResType:'instances',expTreeOpen:{},expLoading:false,expRegions:[],expCounts:{},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='<div style="padding:2rem;color:red"><h3>Render Error</h3><pre>'+e.message+'</pre></div>'}}
|
||||
@@ -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 '<div class="tf-plan-block"><div class="tf-plan-header">'+TF_ICON+' Resource Plan — '+S.tfPlan.length+' resource(s)</div><div class="tf-plan-list">'+
|
||||
S.tfPlan.map(x=>'<div class="tf-plan-item"><span class="tf-plan-add">+</span><span class="tf-plan-type">'+escHtml(x.type)+'</span><span class="tf-plan-desc">'+escHtml(x.desc)+'</span></div>').join('')+'</div></div>';
|
||||
return _renderTfPlanBlock();
|
||||
});
|
||||
// Also match free-form "Resource Plan" / "**Resource Plan**" followed by +/~ lines (model often skips ```plan block)
|
||||
if(!S.tfPlan.length){
|
||||
r=r.replace(/(\*{0,2}Resource Plan\*{0,2})\s*\n((?:[+~][\s\S]*?)(?=\n\n|\n##|\n\*\*|$))/g,(m,hdr,plan)=>{
|
||||
const lines=plan.trim().split('\n').filter(l=>/^[+~]/.test(l.trim()));
|
||||
S.tfPlan=lines.map(l=>{const clean=l.trim().replace(/^[+~]\s*/,'').replace(/\s{2,}/g,' ').trim();return{type:clean,desc:''}}).filter(x=>x.type);
|
||||
return _renderTfPlanBlock();
|
||||
});
|
||||
}
|
||||
function _renderTfPlanBlock(){
|
||||
return '<div class="tf-plan-block"><div class="tf-plan-header">'+TF_ICON+' Resource Plan — '+S.tfPlan.length+' resource(s)</div><div class="tf-plan-list">'+
|
||||
S.tfPlan.map(x=>'<div class="tf-plan-item"><span class="tf-plan-add">+</span><span class="tf-plan-type">'+escHtml(x.type)+'</span>'+(x.desc?'<span class="tf-plan-desc">'+escHtml(x.desc)+'</span>':'')+'</div>').join('')+'</div></div>';
|
||||
}
|
||||
// Auto-split: if model returned 1-2 large blocks, split into multiple files
|
||||
// Runs on BOTH first generation and corrections — monolithic responses always get split
|
||||
let didAutoSplit=false;
|
||||
@@ -945,6 +1024,10 @@ function rTerraform(){
|
||||
<div style="position:relative">
|
||||
<div class="tf-file-btn" onclick="tfToggleMenu()" title="Menu" style="width:24px;height:24px;cursor:pointer"><svg viewBox="0 0 16 16" width="14" height="14" fill="var(--t3)"><path d="M8 4.5a1.5 1.5 0 110-3 1.5 1.5 0 010 3zm0 5a1.5 1.5 0 110-3 1.5 1.5 0 010 3zm0 5a1.5 1.5 0 110-3 1.5 1.5 0 010 3z"/></svg></div>
|
||||
<div class="tf-menu" id="tfMenu" style="display:none">
|
||||
<div class="tf-menu-item" onclick="tfOpenPrompt()">
|
||||
<svg viewBox="0 0 16 16" width="13" height="13" fill="currentColor"><path d="M13.5 2.5l-1-1L3 11l-1 3 3-1L14.5 3.5l-1-1zM2.5 13.5l.7-2.1 1.4 1.4-2.1.7z"/></svg>
|
||||
<span>Editar Prompt</span>
|
||||
</div>
|
||||
<div class="tf-menu-item" onclick="tfRefreshRef()">
|
||||
<svg viewBox="0 0 16 16" width="13" height="13" fill="currentColor"><path d="M8 1a7 7 0 106.33 4h-2.1A5 5 0 108 13a5 5 0 004.24-2.36l1.52 1.02A7 7 0 018 1z"/><path d="M11 1v4h4" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
|
||||
<span>Atualizar Referência TF</span>
|
||||
@@ -986,8 +1069,18 @@ function rTerraform(){
|
||||
</div>
|
||||
</div>
|
||||
${S.tfConfirmDest?rTfConfirmDestroy():''}
|
||||
${S.tfPromptOpen?rTfPromptModal():''}
|
||||
</div>`;
|
||||
}
|
||||
function rTfPromptModal(){
|
||||
const p=S.tfPrompt||{};
|
||||
return`<div class="tf-modal-overlay" onclick="tfClosePrompt()"><div class="tf-modal wide" onclick="event.stopPropagation()">
|
||||
<div class="tf-prompt-header"><h3>Editar Prompt — Terraform Agent</h3><span id="tfPromptSaved" class="tf-prompt-saved" style="opacity:.4">Salvo ✓</span></div>
|
||||
<input class="tf-prompt-name" value="${(p.name||'').replace(/"/g,'"')}" placeholder="Nome do prompt" oninput="tfPromptChanged('name',this.value)">
|
||||
<textarea class="tf-prompt-ta" oninput="tfPromptChanged('content',this.value)">${p.content||''}</textarea>
|
||||
<div style="text-align:right;margin-top:.6rem"><button class="btn bs" onclick="tfClosePrompt()">Fechar</button></div>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
function _tfBadge(st){
|
||||
const m={draft:['draft','Draft'],planning:['run','Planning...'],planned:['ok','Plan OK'],applying:['run','Applying...'],applied:['ok','Applied'],destroying:['run','Destroying...'],destroyed:['dest','Destroyed'],failed:['err','Failed']};
|
||||
@@ -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'<div class="emp"><p>Selecione um compartm
|
||||
return`<div style="font-size:.74rem;color:var(--t4);margin-bottom:.5rem">${d.length} item(ns) encontrado(s)</div><div class="exp-r">${d.map(i=>{
|
||||
const keys=Object.keys(i);
|
||||
const name=i.display_name||i.name||i.db_name||i.application||'—';
|
||||
const skip=['display_name','name','db_name','lifecycle_state','application','rules','routes'];
|
||||
const skip=['display_name','name','db_name','lifecycle_state','application','rules','routes','whitelisted_ips'];
|
||||
const aclHtml=isAdb&&i.whitelisted_ips&&i.whitelisted_ips.length?'<br><span style="color:var(--ac);font-size:.66rem;font-weight:600">ACL:</span> <span class="em" style="font-size:.66rem">'+i.whitelisted_ips.join(', ')+'</span>':'';
|
||||
let actionBtn='';
|
||||
if(isInst&&i.lifecycle_state==='RUNNING')actionBtn=' <button class="btn bsm" style="font-size:.6rem;padding:1px 8px;background:var(--rd);color:#fff;border-color:var(--rd);margin-left:6px" onclick="event.stopPropagation();ociInstanceAction(\''+i.id+'\',\'STOP\')">■ Stop</button>';
|
||||
else if(isInst&&i.lifecycle_state==='STOPPED')actionBtn=' <button class="btn bsm" style="font-size:.6rem;padding:1px 8px;background:var(--gn);color:#fff;border-color:var(--gn);margin-left:6px" onclick="event.stopPropagation();ociInstanceAction(\''+i.id+'\',\'START\')">▶ Start</button>';
|
||||
else if(isAdb&&i.lifecycle_state==='AVAILABLE')actionBtn=' <button class="btn bsm" style="font-size:.6rem;padding:1px 8px;background:var(--rd);color:#fff;border-color:var(--rd);margin-left:6px" onclick="event.stopPropagation();ociAdbAction(\''+i.id+'\',\'stop\')">■ Stop</button>';
|
||||
else if(isAdb&&i.lifecycle_state==='STOPPED')actionBtn=' <button class="btn bsm" style="font-size:.6rem;padding:1px 8px;background:var(--gn);color:#fff;border-color:var(--gn);margin-left:6px" onclick="event.stopPropagation();ociAdbAction(\''+i.id+'\',\'start\')">▶ Start</button>';
|
||||
return`<div class="exp-c"><strong>${name}</strong>${i.lifecycle_state?` <span class="badge ${i.lifecycle_state==='RUNNING'||i.lifecycle_state==='ACTIVE'||i.lifecycle_state==='AVAILABLE'?'b-pass':'b-pend'}">${i.lifecycle_state}</span>`:''}${actionBtn}<br>
|
||||
else if(isAdb&&i.lifecycle_state==='AVAILABLE')actionBtn=' <button class="btn bsm" style="font-size:.6rem;padding:1px 8px;background:var(--rd);color:#fff;border-color:var(--rd);margin-left:6px" onclick="event.stopPropagation();ociAdbAction(\''+i.id+'\',\'stop\')">■ Stop</button> <button class="btn bsm" style="font-size:.6rem;padding:1px 8px;background:var(--ac);color:#fff;border-color:var(--ac);margin-left:2px" onclick="event.stopPropagation();ociAdbUpdateNet(\''+i.id+'\')">🔒 Update Network Access</button>';
|
||||
else if(isAdb&&i.lifecycle_state==='STOPPED')actionBtn=' <button class="btn bsm" style="font-size:.6rem;padding:1px 8px;background:var(--gn);color:#fff;border-color:var(--gn);margin-left:6px" onclick="event.stopPropagation();ociAdbAction(\''+i.id+'\',\'start\')">▶ Start</button> <button class="btn bsm" style="font-size:.6rem;padding:1px 8px;background:var(--ac);color:#fff;border-color:var(--ac);margin-left:2px" onclick="event.stopPropagation();ociAdbUpdateNet(\''+i.id+'\')">🔒 Update Network Access</button>';
|
||||
return`<div class="exp-c"><strong>${name}</strong>${i.lifecycle_state?` <span class="badge ${i.lifecycle_state==='RUNNING'||i.lifecycle_state==='ACTIVE'||i.lifecycle_state==='AVAILABLE'?'b-pass':'b-pend'}">${i.lifecycle_state}</span>`:''}${actionBtn}${aclHtml}<br>
|
||||
${keys.filter(k=>!skip.includes(k)).map(k=>`<span style="color:var(--t4);font-size:.66rem">${k}:</span> <span class="em">${typeof i[k]==='object'?JSON.stringify(i[k]):i[k]}</span>`).join('<br>')}</div>`}).join('')}</div>`}
|
||||
function rExpSecLists(d){
|
||||
return`<div style="font-size:.74rem;color:var(--t4);margin-bottom:.5rem">${d.length} security list(s)</div>${d.map((sl,idx)=>{
|
||||
|
||||
Reference in New Issue
Block a user