feat: session token auth, workspace submenu, retry buttons, terraform fixes, app rename
- Rename app to "AI Agent - Infrastructure & Security Engineer" - OCI session token authentication (SecurityTokenSigner, conditional form, type tags) - Terraform Workspaces sidebar sub-tab (date-grouped accordion, INNER JOIN filter) - Retry button on failed messages (Chat, Terraform, Prompt Generator) - Fix f2 undefined in dedup, oci_core_services false validation, Re-plan tfCode sync - Auto-fix button no longer requires pre-selected model - DRG attachment rules added to Terraform system prompt - Stuck workspace recovery on container restart
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>OCI CIS AI Agent</title>
|
||||
<title>AI Agent - Infrastructure & Security Engineer</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root,html.light {
|
||||
@@ -374,6 +374,8 @@ html .spinner,html [class*="kpi-bar-fill"],html canvas{transition:none!important
|
||||
.exp-cats{flex-wrap:wrap}
|
||||
.exp-cat{padding:.3rem .5rem;font-size:.6rem}
|
||||
}
|
||||
.retry-btn{display:inline-flex;align-items:center;gap:.25rem;margin-top:.35rem;padding:.25rem .65rem;font-size:.64rem;font-family:inherit;border-radius:6px;border:1px solid var(--rd);background:transparent;color:var(--rd);cursor:pointer;transition:all .15s}
|
||||
.retry-btn:hover{background:var(--rd);color:#fff}
|
||||
/* ── Terraform Agent ── */
|
||||
.tf-wrap{display:flex;flex-direction:row;height:calc(100vh - 56px - 3rem);overflow:hidden}
|
||||
.tf-left{flex:6;display:flex;flex-direction:column;overflow:hidden;min-width:0}
|
||||
@@ -454,6 +456,33 @@ html .spinner,html [class*="kpi-bar-fill"],html canvas{transition:none!important
|
||||
.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)}
|
||||
/* ── Terraform Workspaces ── */
|
||||
.tws-page{padding:1rem;height:calc(100vh - 56px - 3rem);overflow-y:auto}
|
||||
.tws-hdr{display:flex;align-items:center;gap:.7rem;margin-bottom:1rem}
|
||||
.tws-hdr h3{margin:0;font-size:.88rem;color:var(--t1)}
|
||||
.tws-filter{display:flex;gap:.3rem}
|
||||
.tws-filter-btn{padding:.2rem .55rem;font-size:.64rem;border-radius:12px;border:1px solid var(--bd);background:var(--bg);color:var(--t3);cursor:pointer;font-family:inherit;transition:all .15s}
|
||||
.tws-filter-btn.on{background:var(--pp);color:#fff;border-color:var(--pp)}
|
||||
.tws-group{margin-bottom:.7rem}
|
||||
.tws-group-hdr{display:flex;align-items:center;gap:.5rem;padding:.4rem .6rem;background:var(--bg2);border:1px solid var(--bd);border-radius:8px;cursor:pointer;user-select:none;transition:background .12s}
|
||||
.tws-group-hdr:hover{background:var(--bg3)}
|
||||
.tws-group-arrow{width:14px;height:14px;transition:transform .2s;fill:var(--t3)}
|
||||
.tws-group-hdr.open .tws-group-arrow{transform:rotate(90deg)}
|
||||
.tws-group-date{font-size:.72rem;font-weight:700;color:var(--t1)}
|
||||
.tws-group-cnt{font-size:.6rem;color:var(--t3);margin-left:auto}
|
||||
.tws-group-body{padding:.3rem 0 0 0}
|
||||
.tws-row{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;border:1px solid var(--bd);border-radius:8px;margin-bottom:.35rem;background:var(--bg);transition:border-color .15s}
|
||||
.tws-row:hover{border-color:var(--pp)}
|
||||
.tws-row-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:.15rem}
|
||||
.tws-row-title{font-size:.74rem;font-weight:600;color:var(--t1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.tws-row-sub{font-size:.6rem;color:var(--t3);display:flex;gap:.6rem;flex-wrap:wrap}
|
||||
.tws-row-sub span{display:inline-flex;align-items:center;gap:.15rem}
|
||||
.tws-row-acts{display:flex;gap:.3rem;align-items:center;flex-shrink:0}
|
||||
.tws-row-acts .btn{font-size:.6rem;padding:2px 8px}
|
||||
.tws-row-detail{margin:.3rem 0 .4rem .7rem;background:var(--bg2);border:1px solid var(--bd);border-radius:6px;padding:.5rem;max-height:200px;overflow:auto}
|
||||
.tws-row-detail pre{font-size:.62rem;line-height:1.4;color:var(--t2);font-family:var(--fm);margin:0;white-space:pre-wrap;word-break:break-word}
|
||||
.tws-empty{text-align:center;padding:3rem 1rem;color:var(--t3)}
|
||||
.tws-empty p{margin:.3rem 0}
|
||||
.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}
|
||||
@media(max-width:768px){
|
||||
@@ -542,7 +571,9 @@ const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],g
|
||||
tfWs:null,tfWsList:[],tfPlanOut:'',tfApplyOut:'',tfDestroyOut:'',tfStatus:'draft',tfRunning:false,tfHadApply:false,tfConfirmDest:false,tfConfirmRollback:false,tfMdOpen:false,
|
||||
tfComps:[],tfCompLoading:false,tfFiles:[],tfBtab:'files',tfEditIdx:-1,tfResources:null,tfResLoading:false,tfRefStatus:'',tfBottomH:null,
|
||||
tfpMsgs:[],tfpLoading:false,tfpCopied:false,tfpModel:'',tfpMdOpen:false,tfpOci:'',tfpRegion:'',tfpCompartment:'',
|
||||
tfpHistOpen:false,tfpHistory:[],tfpSid:null};
|
||||
tfpHistOpen:false,tfpHistory:[],tfpSid:null,
|
||||
twsList:[],twsLoading:false,twsExpanded:null,twsFilter:'all',twsConfirmDel:null,twsConfirmDest:null,twsClosedGroups:null,
|
||||
ociFormOpen:false,ociAuthType:'api_key'};
|
||||
const API='/api';
|
||||
|
||||
async function $api(p,o={}){const h={...(o.headers||{})};if(S.token)h['Authorization']='Bearer '+S.token;
|
||||
@@ -603,13 +634,13 @@ function R(){try{
|
||||
}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>'}}
|
||||
function switchTab(t){S.tab=t;R();
|
||||
const pg=document.getElementById('pg');if(pg){const pc=pg.querySelector('.pc');if(pc){pc.classList.add('tab-enter');setTimeout(()=>pc.classList.remove('tab-enter'),500)}}
|
||||
if(t==='chat')loadHistory('chat');if(t==='terraform')loadHistory('terraform');if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl();if(t==='explorer'&&S.ociCfg.length&&!S.expTree.length){if(!S.expCfg)S.expCfg=S.ociCfg[0].id;expLoadTree();expLoadRegions()}
|
||||
if(t==='chat')loadHistory('chat');if(t==='terraform')loadHistory('terraform');if(t==='tf-workspaces')tfWsLoadList();if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl();if(t==='explorer'&&S.ociCfg.length&&!S.expTree.length){if(!S.expCfg)S.expCfg=S.ociCfg[0].id;expLoadTree();expLoadRegions()}
|
||||
const tm={'oci-config':'oci','genai':'genai','adb':'adb','mcp':'mcp'};if(tm[t])setTimeout(()=>refreshCLogs(tm[t]),100)}
|
||||
|
||||
/* ── Login ── */
|
||||
function rLogin(){return`<div class="lp"><div class="lc fi">
|
||||
<div style="text-align:center;margin-bottom:1.5rem">${LOGO_R}
|
||||
<h2>OCI CIS Agent</h2><div class="st">Oracle Cloud Infrastructure · Security</div></div>
|
||||
<h2>AI Agent</h2><div class="st">Infrastructure & Security Engineer</div></div>
|
||||
<div id="le"></div>
|
||||
<div id="mfa-s" style="display:none"><div class="al al-i">Código MFA obrigatório</div>
|
||||
<div class="ig"><label>Código TOTP</label><input type="text" id="mfa-c" placeholder="000000" maxlength="6" style="text-align:center;font-size:1.1rem;letter-spacing:.3em;font-weight:600"></div>
|
||||
@@ -624,12 +655,12 @@ function rApp(){return`<div class="app">${rSb()}<div class="mc">${rTb()}<div cla
|
||||
|
||||
const TF_IC='<svg viewBox="0 0 16 16" width="14" height="14" style="vertical-align:-2px;margin-right:2px"><path d="M5.6 1v4.2l3.6 2.1V3.1L5.6 1zm4.4 2.1v4.2l3.6-2.1V1L10 3.1zM1.4 3.5v4.2l3.6 2.1V5.6L1.4 3.5zM5.6 8.1v4.2L9.2 14.4V10.2L5.6 8.1z" fill="#7b42bc"/></svg>';
|
||||
function rSb(){
|
||||
const tabs=[['chat',IC.chat,'Chat Agent'],['terraform',TF_IC,'Terraform'],['tf-prompt',IC.edit,'Prompt Generator','sub'],['explorer',IC.search,'OCI Explorer'],['report',IC.chart,'Reports'],['downloads',IC.folder,'Downloads']];
|
||||
const tabs=[['chat',IC.chat,'Chat Agent'],['terraform',TF_IC,'Terraform'],['tf-prompt',IC.edit,'Prompt Generator','sub'],['tf-workspaces',IC.folder,'Workspaces','sub'],['explorer',IC.search,'OCI Explorer'],['report',IC.chart,'Reports'],['downloads',IC.folder,'Downloads']];
|
||||
const ctabs=[['oci-config',IC.cloud,'Credenciais OCI'],['genai',IC.brain,'GenAI Config'],['mcp',IC.plug,'MCP Servers'],['adb',IC.db,'ADB Vector'],['embeddings',IC.dna,'Embeddings'],['emb-consult',IC.search,'Consultar Embeddings','sub']];
|
||||
const atabs=[['users',IC.users,'Usuários'],['mfa',IC.lock,'MFA'],['audit',IC.log,'Audit Log']];
|
||||
const i=(S.user?.first_name||S.user?.username||'?')[0].toUpperCase();
|
||||
return`<div class="sb">
|
||||
<div class="sb-h"><h1>${LOGO_W} OCI CIS Agent</h1><div class="st">Infrastructure & Security · v${V}</div></div>
|
||||
<div class="sb-h"><h1>${LOGO_W} AI Agent</h1><div class="st">Infrastructure & Security Engineer · v${V}</div></div>
|
||||
<div class="nav"><div class="nl">Principal</div>
|
||||
${tabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[0]}')" ${t[3]==='sub'?'style="padding-left:2.2rem;font-size:.78rem;opacity:.9"':''}><span class="ic">${t[1]}</span>${t[2]}</div>`).join('')}
|
||||
<div class="nl">Configuração</div>
|
||||
@@ -640,15 +671,15 @@ ${atabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[
|
||||
<div class="lo-btn" onclick="toggleTheme()" title="Alternar tema">${document.documentElement.classList.contains('dark')?IC.sun:IC.moon}</div>
|
||||
<div class="lo-btn" onclick="doLogout()" title="Sair">⏻</div></div></div></div>`}
|
||||
|
||||
function rTb(){const t={'chat':IC.chat+' AI Agent Chat','terraform':TF_IC+' Terraform Agent','tf-prompt':IC.edit+' Prompt Generator','explorer':IC.search+' OCI Account Explorer','report':IC.chart+' Compliance Reports','downloads':IC.folder+' Downloads','oci-config':IC.cloud+' Credenciais OCI','genai':IC.brain+' OCI Generative AI','mcp':IC.plug+' MCP Servers','adb':IC.db+' Autonomous DB Vector','embeddings':IC.dna+' Embeddings','emb-consult':IC.search+' Consultar Embeddings','users':IC.users+' Gerenciar Usuários','mfa':IC.lock+' Autenticação MFA','audit':IC.log+' Audit Log'};
|
||||
function rTb(){const t={'chat':IC.chat+' AI Agent Chat','terraform':TF_IC+' Terraform Agent','tf-prompt':IC.edit+' Prompt Generator','tf-workspaces':IC.folder+' Terraform Workspaces','explorer':IC.search+' OCI Account Explorer','report':IC.chart+' Compliance Reports','downloads':IC.folder+' Downloads','oci-config':IC.cloud+' Credenciais OCI','genai':IC.brain+' OCI Generative AI','mcp':IC.plug+' MCP Servers','adb':IC.db+' Autonomous DB Vector','embeddings':IC.dna+' Embeddings','emb-consult':IC.search+' Consultar Embeddings','users':IC.users+' Gerenciar Usuários','mfa':IC.lock+' Autenticação MFA','audit':IC.log+' Audit Log'};
|
||||
return`<div class="tb"><div class="tb-t">${t[S.tab]||''}</div><div class="tb-a"><span class="tag">v${V}</span></div></div>`}
|
||||
|
||||
function rPg(){switch(S.tab){case'chat':return rChat();case'terraform':return rTerraform();case'tf-prompt':return rTfPrompt();case'explorer':return rExplorer();case'report':return rReport();case'downloads':return rDl();case'oci-config':return rOci();case'genai':return rGenAI();case'mcp':return rMCP();case'adb':return rADB();case'embeddings':return rEmbeddings();case'emb-consult':return rEmbConsultPage();case'users':return rUsers();case'mfa':return rMfa();case'audit':return rAudit();default:return''}}
|
||||
function rPg(){switch(S.tab){case'chat':return rChat();case'terraform':return rTerraform();case'tf-prompt':return rTfPrompt();case'tf-workspaces':return rTfWorkspaces();case'explorer':return rExplorer();case'report':return rReport();case'downloads':return rDl();case'oci-config':return rOci();case'genai':return rGenAI();case'mcp':return rMCP();case'adb':return rADB();case'embeddings':return rEmbeddings();case'emb-consult':return rEmbConsultPage();case'users':return rUsers();case'mfa':return rMfa();case'audit':return rAudit();default:return''}}
|
||||
|
||||
/* ── Chat ── */
|
||||
function rChat(){
|
||||
const ms=S.msgs.length===0?`<div class="emp"><div class="eic">${IC.bot}</div><p>Inicie uma conversa com o agente.</p><p style="font-size:.74rem;margin-top:.35rem">Selecione um modelo para começar.</p></div>`
|
||||
:S.msgs.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}</div>${m.t?`<div class="cm-ts">${m.t}</div>`:''}</div>`).join('');
|
||||
:S.msgs.map((m,i)=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}</div>${m._failed?`<button class="retry-btn" onclick="chatRetry(${i})">↻ Reenviar</button>`:''}${m.t?`<div class="cm-ts">${m.t}</div>`:''}</div>`).join('');
|
||||
const provs={};for(const[mid,info]of Object.entries(S.models)){const p=info.provider||'other';if(!provs[p])provs[p]=[];provs[p].push({id:mid,name:info.name})}
|
||||
const provOrder=['openai','google','meta','xai'];
|
||||
let ddItems='';
|
||||
@@ -802,19 +833,38 @@ async function sChat(){const el=document.getElementById('chi');const m=el.value.
|
||||
let resp=d.response;
|
||||
if(d.tools_used&&d.tools_used.length){resp+='\n\n'+IC.wrench+' **Tools utilizadas:** '+d.tools_used.map(t=>t.name).join(', ')}
|
||||
S.msgs.push({r:'assistant',c:resp,t:tstamp()});R();scCh()}}
|
||||
catch(e){S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:IC.err+' Erro: '+e.message,t:tstamp()});R()}
|
||||
catch(e){S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:IC.err+' Erro: '+e.message,t:tstamp(),_failed:true});R()}
|
||||
finally{el.disabled=false;el.focus();btns.forEach(b=>{b.disabled=false});if(sendBtn)sendBtn.textContent=sendBtn.dataset.origText||'Enviar →'}}
|
||||
async function pollChatResult(mid){
|
||||
for(let i=0;i<1200;i++){
|
||||
await new Promise(r=>setTimeout(r,i<10?1000:3000));
|
||||
try{const r=await $api('/chat/'+mid+'/status');
|
||||
if(r.status==='done'){S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:r.content,t:tstamp()});R();scCh();return}
|
||||
if(r.status==='failed'){S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:IC.err+' '+r.content,t:tstamp()});R();scCh();return}
|
||||
if(r.status==='failed'){S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:IC.err+' '+r.content,t:tstamp(),_failed:true});R();scCh();return}
|
||||
}catch(e){}}
|
||||
S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:IC.timeout+' Timeout: a resposta está demorando muito. Tente novamente.',t:tstamp()});R()}
|
||||
S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:IC.timeout+' Timeout: a resposta está demorando muito. Tente novamente.',t:tstamp(),_failed:true});R()}
|
||||
function scCh(){setTimeout(()=>{const e=document.getElementById('chm');if(e)e.scrollTop=e.scrollHeight},50)}
|
||||
async function clrChat(){if(S.sid)try{await $api('/chat/'+S.sid,{method:'DELETE'})}catch(e){}S.msgs=[];S.sid=null;R()}
|
||||
function newChat(){S.msgs=[];S.sid=null;S.chatFiles=[];R()}
|
||||
function chatRetry(idx){
|
||||
// Find the last user message before this failed assistant message
|
||||
let userMsg='';
|
||||
for(let i=idx-1;i>=0;i--){if(S.msgs[i].r==='user'){userMsg=S.msgs[i].c;break}}
|
||||
if(!userMsg)return;
|
||||
// Remove the failed message
|
||||
S.msgs.splice(idx,1);R();
|
||||
// Re-send
|
||||
const el=document.getElementById('chi');if(el)el.value=userMsg;
|
||||
sChat();
|
||||
}
|
||||
function tfRetry(idx){
|
||||
let userMsg='';
|
||||
for(let i=idx-1;i>=0;i--){if(S.tfMsgs[i].r==='user'){userMsg=S.tfMsgs[i].c;break}}
|
||||
if(!userMsg)return;
|
||||
S.tfMsgs.splice(idx,1);R();
|
||||
const el=document.getElementById('tfi');if(el)el.value=userMsg;
|
||||
tfSend();
|
||||
}
|
||||
function newTfChat(){S.tfMsgs=[];S.tfSid=null;S.tfPlan=[];S.tfCode='';S.tfFiles=[];S.tfWs=null;S.tfPlanOut='';S.tfApplyOut='';S.tfDestroyOut='';S.tfStatus='draft';S.tfRunning=false;S.tfHadApply=false;S.tfConfirmDest=false;S.tfConfirmRollback=false;S.tfEditIdx=-1;R()}
|
||||
|
||||
function tfToggleMenu(){
|
||||
@@ -1207,7 +1257,7 @@ function rTfPrompt(){
|
||||
<div style="display:flex;flex-wrap:wrap;gap:.35rem;margin-top:.75rem;justify-content:center">${TFP_EXAMPLES.map(e=>`<button class="btn bs bsm" onclick="document.getElementById('tfpi').value='${e.replace(/'/g,"\\'")}';document.getElementById('tfpi').focus()" style="font-size:.66rem;white-space:normal;text-align:left;max-width:280px;line-height:1.35">${e}</button>`).join('')}</div></div>`
|
||||
:msgs.map((m,i)=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}${m._raw?`
|
||||
<div class="tfp-acts"><button class="tfp-act" onclick="tfpCopy(${i})">${m._copied?IC.ok+' Copiado':IC.clip+' Copiar'}</button>
|
||||
<button class="tfp-act tfp-act-tf" onclick="tfpSendToTf(${i})">${TF_IC} Terraform</button></div>`:''}</div></div>`).join('');
|
||||
<button class="tfp-act tfp-act-tf" onclick="tfpSendToTf(${i})">${TF_IC} Terraform</button></div>`:''}</div>${m._failed?`<button class="retry-btn" onclick="tfpRetry(${i})">↻ Reenviar</button>`:''}</div>`).join('');
|
||||
// Model dropdown — only recommended models, grouped by provider
|
||||
const tfpAllowed=['openai.gpt-4.1','openai.o3','openai.o4-mini','openai.gpt-5.1','openai.gpt-5.2','google.gemini-2.5-pro','google.gemini-2.5-flash'];
|
||||
const tfpProvs={};tfpAllowed.forEach(mid=>{const mi=S.models[mid];if(mi){const p=mi.provider||'other';if(!tfpProvs[p])tfpProvs[p]=[];tfpProvs[p].push({id:mid,name:mi.name})}});
|
||||
@@ -1257,10 +1307,26 @@ async function tfpSend(){
|
||||
if(S.tfpSid)body.session_id=S.tfpSid;
|
||||
const d=await $api('/terraform/generate-prompt',{method:'POST',body});
|
||||
if(d.session_id)S.tfpSid=d.session_id;
|
||||
S.tfpMsgs.push({r:'assistant',c:d.prompt,_raw:d.prompt});
|
||||
}catch(e){S.tfpMsgs.push({r:'assistant',c:IC.err+' Erro: '+e.message})}
|
||||
S.tfpLoading=false;R();
|
||||
const mc2=document.getElementById('tfpchm');if(mc2)mc2.scrollTop=mc2.scrollHeight}
|
||||
if(d.status==='processing'&&d.message_id){await tfpPollResult(d.message_id)}
|
||||
else if(d.prompt){S.tfpMsgs.push({r:'assistant',c:d.prompt,_raw:d.prompt});S.tfpLoading=false;R()}
|
||||
}catch(e){S.tfpMsgs.push({r:'assistant',c:IC.err+' Erro: '+e.message,_failed:true});S.tfpLoading=false;R()}}
|
||||
async function tfpPollResult(mid){
|
||||
for(let i=0;i<600;i++){
|
||||
await new Promise(r=>setTimeout(r,i<10?1000:3000));
|
||||
try{const r=await $api('/chat/'+mid+'/status');
|
||||
if(r.status==='done'){S.tfpMsgs.push({r:'assistant',c:r.content,_raw:r.content});S.tfpLoading=false;R();
|
||||
const mc=document.getElementById('tfpchm');if(mc)mc.scrollTop=mc.scrollHeight;return}
|
||||
if(r.status==='failed'){S.tfpMsgs.push({r:'assistant',c:IC.err+' '+r.content,_failed:true});S.tfpLoading=false;R();return}
|
||||
}catch(e){}}
|
||||
S.tfpMsgs.push({r:'assistant',c:IC.timeout+' Timeout: a geração está demorando muito.',_failed:true});S.tfpLoading=false;R()}
|
||||
function tfpRetry(idx){
|
||||
let userMsg='';
|
||||
for(let i=idx-1;i>=0;i--){if(S.tfpMsgs[i].r==='user'){userMsg=S.tfpMsgs[i].c;break}}
|
||||
if(!userMsg)return;
|
||||
S.tfpMsgs.splice(idx,1);R();
|
||||
const el=document.getElementById('tfpi');if(el)el.value=userMsg;
|
||||
tfpSend();
|
||||
}
|
||||
function tfpPickModel(v){S.tfpModel=v;S.tfpMdOpen=false;
|
||||
if(v&&!v.startsWith('cfg:')&&!S.tfpOci&&S.ociCfg.length){S.tfpOci=S.ociCfg[0].id;S.tfpRegion=S.ociCfg[0].region;S.tfpCompartment=S.ociCfg[0].compartment_id||''}
|
||||
else if(v&&v.startsWith('cfg:')){const g=S.genaiCfg.find(x=>x.id===v.slice(4));if(g&&g.oci_config_id&&!S.tfpOci){S.tfpOci=g.oci_config_id;const c=S.ociCfg.find(x=>x.id===g.oci_config_id);if(c){S.tfpRegion=c.region;S.tfpCompartment=c.compartment_id||''}}}
|
||||
@@ -1292,7 +1358,7 @@ function tfpSendToTf(idx){
|
||||
function rTerraform(){
|
||||
const ms=S.tfMsgs.length===0
|
||||
?`<div class="tf-empty">${TF_ICON}<p>Descreva a infraestrutura OCI desejada</p><p style="font-size:.68rem;opacity:.6">Ex: "Crie uma VCN com subnets pública e privada, internet gateway e um compute instance"</p></div>`
|
||||
:S.tfMsgs.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${m.r==='assistant'?fmTf(m.c):fm(m.c)}</div>${m.t?`<div class="cm-ts">${m.t}</div>`:''}</div>`).join('');
|
||||
:S.tfMsgs.map((m,i)=>`<div class="cm cm-${m.r}"><div class="cb">${m.r==='assistant'?fmTf(m.c):fm(m.c)}</div>${m._failed?`<button class="retry-btn" onclick="tfRetry(${i})">↻ Reenviar</button>`:''}${m.t?`<div class="cm-ts">${m.t}</div>`:''}</div>`).join('');
|
||||
|
||||
// Model dropdown — same filtered models as Prompt Generator, grouped by provider
|
||||
const tfAllowed=['openai.gpt-4.1','openai.o3','openai.o4-mini','openai.gpt-5.1','openai.gpt-5.2','google.gemini-2.5-pro','google.gemini-2.5-flash'];
|
||||
@@ -1405,7 +1471,7 @@ function rTfActions(){
|
||||
const canReplan=S.tfCode&&['planned','failed','applied'].includes(st);
|
||||
if(canPlan&&!canReplan)btns+=`<button class="btn bsm" style="background:var(--pp);color:#fff;border-color:var(--pp);font-size:.72rem;padding:4px 12px;gap:4px;display:inline-flex;align-items:center" onclick="tfSaveAndPlan()">${TF_ICON} Plan</button>`;
|
||||
if(canReplan)btns+=`<button class="btn bsm" style="background:var(--pp);color:#fff;border-color:var(--pp);font-size:.72rem;padding:4px 12px;gap:4px;display:inline-flex;align-items:center" onclick="tfSaveAndPlan()">${TF_ICON} Re-plan</button>`;
|
||||
if(st==='failed'&&S.tfPlanOut&&S.tfFiles.length&&S.tfModel)btns+=`<button class="btn bsm" style="background:#e67e22;color:#fff;border-color:#e67e22;font-size:.72rem;padding:4px 12px;gap:4px;display:inline-flex;align-items:center" onclick="tfAutoFix()">${IC.fix} Auto-corrigir</button>`;
|
||||
if(st==='failed'&&S.tfPlanOut&&S.tfFiles.length)btns+=`<button class="btn bsm" style="background:#e67e22;color:#fff;border-color:#e67e22;font-size:.72rem;padding:4px 12px;gap:4px;display:inline-flex;align-items:center" onclick="tfAutoFix()">${IC.fix} Auto-corrigir</button>`;
|
||||
if(st==='failed'&&(S.tfApplyOut||S.tfHadApply))btns+='<button class="btn bsm" style="background:var(--rd);color:#fff;border-color:var(--rd);font-size:.72rem;padding:4px 12px;gap:4px;display:inline-flex;align-items:center" onclick="S.tfConfirmRollback=true;R()">↩ Rollback</button>';
|
||||
if(st==='planned')btns+='<button class="btn bsm" style="background:var(--gn);color:#fff;border-color:var(--gn);font-size:.72rem;padding:4px 12px" onclick="tfApply()">✓ Apply</button>';
|
||||
if(st==='applied'||st==='planned')btns+='<button class="btn bsm" style="background:var(--rd);color:#fff;border-color:var(--rd);font-size:.72rem;padding:4px 12px" onclick="S.tfConfirmDest=true;R()">✕ Destroy</button>';
|
||||
@@ -1699,7 +1765,7 @@ async function tfSend(){
|
||||
if(S.tfHistOpen)loadHistory('terraform');
|
||||
if(d.status==='processing'&&d.message_id){await tfPollResult(d.message_id)}
|
||||
else{S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:d.response||d.content||'Sem resposta',t:tstamp()});R();tfScroll()}
|
||||
}catch(e){S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:IC.err+' Erro: '+e.message,t:tstamp()});R()}
|
||||
}catch(e){S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:IC.err+' Erro: '+e.message,t:tstamp(),_failed:true});R()}
|
||||
}
|
||||
|
||||
async function tfPollResult(mid){
|
||||
@@ -1708,13 +1774,17 @@ async function tfPollResult(mid){
|
||||
try{const r=await $api('/chat/'+mid+'/status');
|
||||
if(r.status==='done'){S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:r.content,t:tstamp()});R();tfScroll();
|
||||
if(r.content&&r.content.includes('```plan')){S.tfPanel='plan';R()}return}
|
||||
if(r.status==='failed'){S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:IC.err+' '+r.content,t:tstamp()});R();tfScroll();return}
|
||||
if(r.status==='failed'){S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:IC.err+' '+r.content,t:tstamp(),_failed:true});R();tfScroll();return}
|
||||
}catch(e){}
|
||||
}
|
||||
S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:IC.timeout+' Timeout.',t:tstamp()});R()
|
||||
S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:IC.timeout+' Timeout.',t:tstamp(),_failed:true});R()
|
||||
}
|
||||
|
||||
async function tfSaveAndPlan(){
|
||||
// Build tfCode from files if empty but files exist
|
||||
if(!S.tfCode&&S.tfFiles.length){
|
||||
S.tfCode=S.tfFiles.map(f=>'// filename: '+f.name+'\n'+f.content).join('\n\n');
|
||||
}
|
||||
if(!S.tfCode){alert('Gere código Terraform primeiro.');return}
|
||||
// Resolve OCI config
|
||||
let ociId=S.tfOci;
|
||||
@@ -1733,7 +1803,7 @@ async function tfSaveAndPlan(){
|
||||
await $api('/terraform/workspaces/'+S.tfWs+'/plan',{method:'POST'});
|
||||
S.tfStatus='planning';S.tfRunning=true;S.tfPlanOut='';S.tfApplyOut='';S.tfDestroyOut='';S.tfBtab='plan';R();
|
||||
await tfPollExec();
|
||||
}catch(e){alert('Erro: '+e.message)}
|
||||
}catch(e){alert('Erro ao executar plan: '+e.message)}
|
||||
}
|
||||
|
||||
async function tfApply(){
|
||||
@@ -1764,7 +1834,8 @@ async function tfCancel(){
|
||||
}
|
||||
|
||||
async function tfAutoFix(){
|
||||
if(!S.tfPlanOut||!S.tfFiles.length||!S.tfModel)return;
|
||||
if(!S.tfPlanOut||!S.tfFiles.length)return;
|
||||
if(!S.tfModel){S.tfMsgs.push({r:'assistant',c:IC.warn+' Selecione um modelo antes de usar Auto-corrigir.',t:tstamp()});R();return}
|
||||
// Extract errors from plan output
|
||||
const errLines=S.tfPlanOut.split('\n').filter(l=>/^Error:|^\s+on\s+\S+\.tf\s+line|^\s+\d+:|has not been declared|is not expected|Missing required|Unsupported|Invalid|Duplicate|undeclared/i.test(l));
|
||||
const errCtx=errLines.length?errLines.join('\n'):S.tfPlanOut.slice(-2000);
|
||||
@@ -1820,6 +1891,219 @@ async function tfPollExec(){
|
||||
}
|
||||
|
||||
|
||||
/* ── Terraform Workspaces ── */
|
||||
async function tfWsLoadList(){
|
||||
S.twsLoading=true;R();
|
||||
try{S.twsList=await $api('/terraform/workspaces')}catch(e){S.twsList=[]}
|
||||
S.twsLoading=false;R();
|
||||
}
|
||||
function rTfWorkspaces(){
|
||||
const f=S.twsFilter;
|
||||
let items=S.twsList||[];
|
||||
if(f==='active')items=items.filter(w=>['applied','planned','planning','applying'].includes(w.status));
|
||||
else if(f==='destroyed')items=items.filter(w=>w.status==='destroyed');
|
||||
else if(f==='failed')items=items.filter(w=>w.status==='failed');
|
||||
else if(f==='draft')items=items.filter(w=>w.status==='draft');
|
||||
const counts={all:(S.twsList||[]).length,active:(S.twsList||[]).filter(w=>['applied','planned','planning','applying'].includes(w.status)).length,destroyed:(S.twsList||[]).filter(w=>w.status==='destroyed').length,failed:(S.twsList||[]).filter(w=>w.status==='failed').length,draft:(S.twsList||[]).filter(w=>w.status==='draft').length};
|
||||
const fb=(k,l)=>`<button class="tws-filter-btn${f===k?' on':''}" onclick="S.twsFilter='${k}';R()">${l} (${counts[k]})</button>`;
|
||||
// Group by date
|
||||
const groups={};
|
||||
for(const w of items){
|
||||
const dt=(w.updated_at||w.created_at||'').slice(0,10);
|
||||
const label=_twsDateLabel(dt);
|
||||
if(!groups[label])groups[label]={date:dt,items:[]};
|
||||
groups[label].items.push(w);
|
||||
}
|
||||
const groupKeys=Object.keys(groups);
|
||||
let body='';
|
||||
if(groupKeys.length){
|
||||
body=groupKeys.map(label=>{
|
||||
const g=groups[label];
|
||||
const open=S.twsClosedGroups?!S.twsClosedGroups.has(label):true;
|
||||
return`<div class="tws-group">
|
||||
<div class="tws-group-hdr${open?' open':''}" onclick="twsToggleGroup('${label.replace(/'/g,"\\'")}')">
|
||||
<svg class="tws-group-arrow" viewBox="0 0 16 16"><path d="M6 3l5 5-5 5z"/></svg>
|
||||
<span class="tws-group-date">${label}</span>
|
||||
<span class="tws-group-cnt">${g.items.length} workspace${g.items.length>1?'s':''}</span>
|
||||
</div>
|
||||
${open?`<div class="tws-group-body">${g.items.map(w=>_rTwsRow(w)).join('')}</div>`:''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}else{
|
||||
body=S.twsLoading?'<div class="tws-empty"><p>Carregando workspaces...</p></div>'
|
||||
:'<div class="tws-empty"><div style="font-size:2rem;margin-bottom:.5rem">${TF_IC}</div><p>Nenhum workspace encontrado</p><p style="font-size:.7rem">Crie workspaces pelo Terraform Agent</p></div>';
|
||||
}
|
||||
return`<div class="tws-page">
|
||||
<div class="tws-hdr">
|
||||
<h3>${TF_IC} Terraform Workspaces</h3>
|
||||
<div style="flex:1"></div>
|
||||
<div class="tws-filter">${fb('all','Todos')}${fb('active','Ativos')}${fb('draft','Draft')}${fb('failed','Falha')}${fb('destroyed','Destruídos')}</div>
|
||||
<button class="btn bs bsm" onclick="tfWsLoadList()" style="font-size:.66rem;padding:3px 10px">${S.twsLoading?'Carregando...':'Atualizar'}</button>
|
||||
</div>
|
||||
${body}
|
||||
${S.twsConfirmDest?_rTwsConfirmDestroy():''}
|
||||
${S.twsConfirmDel?_rTwsConfirmDelete():''}
|
||||
</div>`;
|
||||
}
|
||||
function _twsDateLabel(dt){
|
||||
if(!dt)return'Sem data';
|
||||
const today=new Date().toISOString().slice(0,10);
|
||||
const yesterday=new Date(Date.now()-86400000).toISOString().slice(0,10);
|
||||
if(dt===today)return'Hoje';
|
||||
if(dt===yesterday)return'Ontem';
|
||||
const d=new Date(dt+'T00:00:00');
|
||||
const diff=Math.floor((new Date()-d)/86400000);
|
||||
if(diff<7)return d.toLocaleDateString('pt-BR',{weekday:'long'});
|
||||
return d.toLocaleDateString('pt-BR',{day:'2-digit',month:'short',year:'numeric'});
|
||||
}
|
||||
function twsToggleGroup(label){
|
||||
if(!S.twsClosedGroups)S.twsClosedGroups=new Set();
|
||||
if(S.twsClosedGroups.has(label))S.twsClosedGroups.delete(label);else S.twsClosedGroups.add(label);
|
||||
R();
|
||||
}
|
||||
function _rTwsRow(w){
|
||||
const badge=_tfBadge(w.status);
|
||||
const oc=S.ociCfg.find(x=>x.id===w.oci_config_id);
|
||||
const ocName=oc?(oc.name||oc.region||w.oci_config_id):'—';
|
||||
const exp=S.twsExpanded===w.id;
|
||||
const ago=_twsTimeAgo(w.updated_at||w.created_at);
|
||||
const hasOutput=w.plan_output||w.apply_output||w.destroy_output||w.error;
|
||||
const title=w.session_title||w.name||'workspace';
|
||||
let acts='';
|
||||
const running=['planning','applying','destroying'].includes(w.status);
|
||||
if(running){acts+=`<button class="btn bsm" style="background:var(--rd);color:#fff;border-color:var(--rd)" onclick="event.stopPropagation();twsCancel('${w.id}')">Cancelar</button>`}
|
||||
else{
|
||||
acts+=`<button class="btn bs bsm" onclick="event.stopPropagation();twsOpen('${w.id}')" title="Abrir no Terraform Agent">Abrir</button>`;
|
||||
if(['draft','failed','destroyed','planned','applied'].includes(w.status))acts+=`<button class="btn bsm" style="background:var(--pp);color:#fff;border-color:var(--pp)" onclick="event.stopPropagation();twsPlan('${w.id}')">Plan</button>`;
|
||||
if(w.status==='planned')acts+=`<button class="btn bsm" style="background:var(--gn);color:#fff;border-color:var(--gn)" onclick="event.stopPropagation();twsApply('${w.id}')">Apply</button>`;
|
||||
if(w.status==='applied'||w.status==='planned')acts+=`<button class="btn bsm" style="background:var(--rd);color:#fff;border-color:var(--rd)" onclick="event.stopPropagation();S.twsConfirmDest='${w.id}';R()">Destroy</button>`;
|
||||
acts+=`<button class="btn bsm" style="color:var(--rd);border-color:var(--rd);background:transparent" onclick="event.stopPropagation();S.twsConfirmDel='${w.id}';R()" title="Excluir">✕</button>`;
|
||||
}
|
||||
let detail='';
|
||||
if(exp&&hasOutput){
|
||||
const out=w.error?'ERROR: '+w.error:(w.destroy_output||w.apply_output||w.plan_output||'');
|
||||
detail=`<div class="tws-row-detail"><pre>${escHtml(out)}</pre></div>`;
|
||||
}
|
||||
return`<div>
|
||||
<div class="tws-row" onclick="${hasOutput&&!running?`S.twsExpanded=S.twsExpanded==='${w.id}'?null:'${w.id}';R()`:''}">
|
||||
${badge}
|
||||
<div class="tws-row-info">
|
||||
<div class="tws-row-title" title="${escHtml(title)}">${escHtml(title)}</div>
|
||||
<div class="tws-row-sub">
|
||||
<span>${IC.cloud} ${escHtml(ocName)}</span>
|
||||
${w.compartment_id?`<span title="${w.compartment_id}">...${w.compartment_id.slice(-8)}</span>`:''}
|
||||
<span>${ago}</span>
|
||||
${hasOutput&&!running?`<span style="color:var(--pp);cursor:pointer">${exp?'▾ ocultar':'▸ ver output'}</span>`:''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tws-row-acts">${acts}</div>
|
||||
</div>
|
||||
${detail}
|
||||
</div>`;
|
||||
}
|
||||
function _twsTimeAgo(ts){
|
||||
if(!ts)return'—';
|
||||
const d=new Date(ts+'Z');const now=new Date();const s=Math.floor((now-d)/1000);
|
||||
if(s<60)return'agora';if(s<3600)return Math.floor(s/60)+'min';if(s<86400)return Math.floor(s/3600)+'h';return Math.floor(s/86400)+'d';
|
||||
}
|
||||
function _rTwsConfirmDestroy(){
|
||||
return`<div class="tf-modal-overlay" onclick="S.twsConfirmDest=null;R()"><div class="tf-modal" onclick="event.stopPropagation()">
|
||||
<h3>Confirmar Destroy</h3>
|
||||
<p style="font-size:.76rem;color:var(--t2);margin:.5rem 0">Esta ação irá destruir todos os recursos provisionados por este workspace. Digite <b>DESTROY</b> para confirmar:</p>
|
||||
<input type="text" id="twsDestInput" placeholder="DESTROY" style="width:100%;padding:.4rem;font-size:.8rem;text-align:center;border:1px solid var(--rd);border-radius:6px;background:var(--bg2);color:var(--t1);box-sizing:border-box">
|
||||
<div style="display:flex;gap:.5rem;justify-content:flex-end;margin-top:.7rem">
|
||||
<button class="btn bs" onclick="S.twsConfirmDest=null;R()">Cancelar</button>
|
||||
<button class="btn" style="background:var(--rd);color:#fff;border-color:var(--rd)" onclick="twsDoDestroy()">Destroy</button>
|
||||
</div>
|
||||
</div></div>`;
|
||||
}
|
||||
function _rTwsConfirmDelete(){
|
||||
return`<div class="tf-modal-overlay" onclick="S.twsConfirmDel=null;R()"><div class="tf-modal" onclick="event.stopPropagation()">
|
||||
<h3>Excluir Workspace</h3>
|
||||
<p style="font-size:.76rem;color:var(--t2);margin:.5rem 0">Deseja excluir este workspace permanentemente? Arquivos locais serão removidos.</p>
|
||||
<div style="display:flex;gap:.5rem;justify-content:flex-end;margin-top:.7rem">
|
||||
<button class="btn bs" onclick="S.twsConfirmDel=null;R()">Cancelar</button>
|
||||
<button class="btn" style="background:var(--rd);color:#fff;border-color:var(--rd)" onclick="twsDoDelete()">Excluir</button>
|
||||
</div>
|
||||
</div></div>`;
|
||||
}
|
||||
async function twsOpen(wid){
|
||||
const ws=S.twsList.find(w=>w.id===wid);if(!ws)return;
|
||||
S.tab='terraform';
|
||||
S.tfSid=ws.session_id;S.tfWs=wid;S.tfStatus=ws.status;
|
||||
try{
|
||||
const detail=await $api('/terraform/workspaces/'+wid);
|
||||
if(detail.tf_code)S.tfCode=detail.tf_code;
|
||||
if(detail.plan_output)S.tfPlanOut=detail.plan_output;
|
||||
if(detail.apply_output)S.tfApplyOut=detail.apply_output;
|
||||
if(detail.destroy_output)S.tfDestroyOut=detail.destroy_output;
|
||||
S.tfOci=detail.oci_config_id||'';S.tfCompartment=detail.compartment_id||'';
|
||||
if(detail.oci_config_id){
|
||||
const oc=S.ociCfg.find(x=>x.id===detail.oci_config_id);
|
||||
if(oc)S.tfRegion=oc.region;
|
||||
}
|
||||
if(detail.tf_code){
|
||||
S.tfCode=detail.tf_code;
|
||||
const parts=detail.tf_code.split(/^\/\/\s*filename:\s*/m).filter(Boolean);
|
||||
if(parts.length>1){S.tfFiles=parts.map(p=>{const nl=p.indexOf('\n');const fn=p.substring(0,nl).trim();const code=p.substring(nl+1).trim();return{name:fn,content:code,size:code.length}})}
|
||||
else{const split=_splitTfMonolith(detail.tf_code);S.tfFiles=Object.entries(split).map(([fn,code])=>({name:fn,content:code,size:code.length}))}
|
||||
}
|
||||
if(detail.status==='applied')S.tfHadApply=true;
|
||||
if(S.tfPlanOut)S.tfBtab='plan';
|
||||
else if(S.tfFiles.length)S.tfBtab='files';
|
||||
if(S.tfOci){tfLoadComps();if(S.tfCompartment)tfLoadResources()}
|
||||
}catch(e){}
|
||||
try{const msgs=await $api('/chat/sessions/'+ws.session_id+'/messages');S.tfMsgs=msgs.map(m=>({r:m.role,c:m.content,t:m.created_at?new Date(m.created_at+'Z').toLocaleTimeString():''}))}catch(e){S.tfMsgs=[]}
|
||||
R();
|
||||
}
|
||||
async function twsPlan(wid){
|
||||
try{await $api('/terraform/workspaces/'+wid+'/plan',{method:'POST'})}catch(e){alert('Erro: '+e.message);return}
|
||||
tfWsLoadList();_twsPollWs(wid);
|
||||
}
|
||||
async function twsApply(wid){
|
||||
try{await $api('/terraform/workspaces/'+wid+'/apply',{method:'POST'})}catch(e){alert('Erro: '+e.message);return}
|
||||
tfWsLoadList();_twsPollWs(wid);
|
||||
}
|
||||
async function twsDoDestroy(){
|
||||
const wid=S.twsConfirmDest;if(!wid)return;
|
||||
const v=document.getElementById('twsDestInput');if(!v||v.value!=='DESTROY'){v.style.borderColor='var(--rd)';return}
|
||||
S.twsConfirmDest=null;
|
||||
try{await $api('/terraform/workspaces/'+wid+'/destroy',{method:'POST',body:{confirm:'DESTROY'}})}catch(e){alert('Erro: '+e.message);R();return}
|
||||
tfWsLoadList();_twsPollWs(wid);
|
||||
}
|
||||
async function twsDoDelete(){
|
||||
const wid=S.twsConfirmDel;if(!wid)return;
|
||||
S.twsConfirmDel=null;
|
||||
try{await $api('/terraform/workspaces/'+wid,{method:'DELETE'})}catch(e){alert('Erro: '+e.message)}
|
||||
tfWsLoadList();
|
||||
}
|
||||
async function twsCancel(wid){
|
||||
try{await $api('/terraform/workspaces/'+wid+'/cancel',{method:'POST'})}catch(e){}
|
||||
tfWsLoadList();
|
||||
}
|
||||
async function _twsPollWs(wid){
|
||||
for(let i=0;i<300;i++){
|
||||
await new Promise(r=>setTimeout(r,3000));
|
||||
try{
|
||||
const r=await $api('/terraform/workspaces/'+wid+'/status');
|
||||
const done=!['planning','applying','destroying'].includes(r.status);
|
||||
const idx=S.twsList.findIndex(w=>w.id===wid);
|
||||
if(idx>=0){
|
||||
S.twsList[idx].status=r.status;
|
||||
S.twsList[idx].plan_output=r.plan_output||S.twsList[idx].plan_output;
|
||||
S.twsList[idx].apply_output=r.apply_output||S.twsList[idx].apply_output;
|
||||
S.twsList[idx].destroy_output=r.destroy_output||S.twsList[idx].destroy_output;
|
||||
S.twsList[idx].error=r.error||'';
|
||||
if(S.tab==='tf-workspaces'){
|
||||
const el=document.getElementById('tws-badge-'+wid);
|
||||
if(el)el.outerHTML=_tfBadge(r.status);
|
||||
}
|
||||
}
|
||||
if(done){tfWsLoadList();return}
|
||||
}catch(e){}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── OCI Explorer ── */
|
||||
const EXP_GROUPS=[
|
||||
{name:'Compute',tabs:[
|
||||
@@ -1858,17 +2142,17 @@ function rExplorer(){
|
||||
const catIcons={Compute:IC.computer,Rede:IC.globe,Storage:IC.hdd,Database:IC.db,Containers:IC.container,Serverless:IC.zap,Observabilidade:IC.activity,Segurança:IC.shield,IAM:IC.user};
|
||||
const cats=EXP_GROUPS.map(g=>`<div class="exp-cat${S.expCat===g.name?' active':''}" onclick="expSetCat('${g.name}')">${catIcons[g.name]||''} ${g.name}</div>`).join('');
|
||||
const activeGrp=EXP_GROUPS.find(g=>g.name===S.expCat)||EXP_GROUPS[0];
|
||||
const tabs=activeGrp.tabs.map(t=>{const cnt=S.expCounts[t.key];return`<div class="exp-tab${S.expResType===t.key?' active':''}" onclick="expSetTab('${t.key}')">${t.label}${cnt!=null?' <span style="font-size:.58rem;opacity:.7">('+cnt+')</span>':''}</div>`}).join('');
|
||||
const tabs=activeGrp.tabs.map(t=>{const cnt=S.expCounts[t.key];return`<div class="exp-tab${S.expResType===t.key?' active':''}" onclick="expSetTab('${t.key}')">${t.label} <span id="exp-tab-cnt-${t.key}" style="font-size:.58rem;opacity:.7">${cnt!=null?'('+cnt+')':''}</span></div>`}).join('');
|
||||
// KPI stats from counts
|
||||
const _iamKeys=new Set(['iam_users','iam_groups','iam_policies','iam_dynamic_groups']);
|
||||
const totalRes=Object.entries(S.expCounts).reduce((a,[k,v])=>a+(_iamKeys.has(k)?0:(v||0)),0);
|
||||
const kpiCats=EXP_GROUPS.map(g=>{const cnt=g.tabs.reduce((a,t)=>a+(S.expCounts[t.key]||0),0);const detail=g.tabs.filter(t=>(S.expCounts[t.key]||0)>0).map(t=>t.label+': '+S.expCounts[t.key]).join('\n');return{name:g.name,icon:catIcons[g.name]||'',count:cnt,detail:detail}}).filter(k=>k.count>0);
|
||||
const selCompName=S.expTree.find(n=>n.id===S.expSelComp);
|
||||
const compLabel=selCompName?selCompName.name:'—';
|
||||
const kpiHtml=S.expSelComp?`<div style="display:flex;gap:.5rem;padding:.5rem .75rem;border-bottom:1px solid var(--bd);background:var(--bg);overflow-x:auto;align-items:stretch">
|
||||
const kpiHtml=S.expSelComp?`<div id="exp-kpi" style="display:flex;gap:.5rem;padding:.5rem .75rem;border-bottom:1px solid var(--bd);background:var(--bg);overflow-x:auto;align-items:stretch">
|
||||
<div class="kpi" style="padding:.5rem .7rem;min-width:120px;flex:1"><div class="kpi-label">${IC.folder} Compartment</div><div style="font-size:.82rem;font-weight:700;color:var(--t1);margin-top:.15rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${compLabel}">${compLabel}</div><div class="kpi-sub">${S.expSelRegions.length||'Todas'} região(ões)</div></div>
|
||||
<div class="kpi kpi-info" style="padding:.5rem .7rem;min-width:80px;text-align:center;flex:0 0 auto"><div class="kpi-label">${IC.activity} Total</div><div class="kpi-val v-info" style="font-size:1.3rem">${totalRes}</div><div class="kpi-sub">${S.expCountsRefreshing?'<span class="spinner" style="width:10px;height:10px;display:inline-block;vertical-align:-1px"></span> atualizando...':'recursos'}</div></div>
|
||||
${kpiCats.slice(0,6).map(k=>`<div class="kpi" style="padding:.5rem .7rem;min-width:75px;text-align:center;flex:0 0 auto;cursor:default" title="${k.detail}"><div class="kpi-label" style="justify-content:center">${k.icon}</div><div class="kpi-val" style="font-size:1.2rem">${k.count}</div><div class="kpi-sub">${k.name}</div></div>`).join('')}
|
||||
<div class="kpi kpi-info" style="padding:.5rem .7rem;min-width:80px;text-align:center;flex:0 0 auto"><div class="kpi-label">${IC.activity} Total</div><div id="exp-kpi-total" class="kpi-val v-info" style="font-size:1.3rem">${totalRes}</div><div id="exp-kpi-sub" class="kpi-sub">${S.expCountsRefreshing?'<span class="spinner" style="width:10px;height:10px;display:inline-block;vertical-align:-1px"></span> atualizando...':'recursos'}</div></div>
|
||||
<span id="exp-kpi-cats">${kpiCats.slice(0,6).map(k=>`<div class="kpi" style="padding:.5rem .7rem;min-width:75px;text-align:center;flex:0 0 auto;cursor:default" title="${k.detail}"><div class="kpi-label" style="justify-content:center">${k.icon}</div><div class="kpi-val" style="font-size:1.2rem">${k.count}</div><div class="kpi-sub">${k.name}</div></div>`).join('')}</span>
|
||||
</div>`:'';
|
||||
return`<div class="exp-wrap">
|
||||
<div class="exp-tree" id="exp-tree"><div style="padding:.6rem .7rem .5rem;font-size:.78rem;font-weight:700;color:var(--t1);border-bottom:1px solid var(--bd);margin-bottom:.3rem;display:flex;align-items:center;gap:.4rem;background:var(--bg2)">${IC.folder} Compartments</div>${S.expTree.length?rExpTree(null,0):'<div class="ld"><span class="spinner"></span> Carregando...</div>'}</div>
|
||||
@@ -1945,31 +2229,44 @@ async function expLoadRes(){
|
||||
S.expCounts[S.expResType]=all.length;
|
||||
S.expLoading=false;R()}
|
||||
let _expCountAbort=0;
|
||||
async function expLoadCachedCounts(){
|
||||
async function expLoadCachedCounts(soft){
|
||||
const comp=S.expSelComp;const cfg=S.expCfg;if(!comp||!cfg)return;
|
||||
const regKey=S.expSelRegions.length?S.expSelRegions.slice().sort().join(','):'';
|
||||
try{
|
||||
const q=new URLSearchParams({compartment_id:comp,regions:regKey});
|
||||
const d=await $api('/oci/explore/'+cfg+'/counts?'+q);
|
||||
if(d&&!d.error){for(const[k,v]of Object.entries(d)){S.expCounts[k]=v.count}R()}
|
||||
if(d&&!d.error){let changed=false;for(const[k,v]of Object.entries(d)){if(S.expCounts[k]!==v.count){S.expCounts[k]=v.count;changed=true}}if(changed){if(soft)expUpdateKpi();else R()}}
|
||||
}catch(e){console.warn('Cache counts error:',e)}
|
||||
}
|
||||
function expUpdateKpi(){
|
||||
const el=document.getElementById('exp-kpi');if(!el)return;
|
||||
const catIcons={Compute:IC.computer,Rede:IC.globe,Storage:IC.hdd,Database:IC.db,Containers:IC.container,Serverless:IC.zap,Observabilidade:IC.activity,'Segurança':IC.shield,IAM:IC.user};
|
||||
const _iamKeys=new Set(['iam_users','iam_groups','iam_policies','iam_dynamic_groups']);
|
||||
const totalRes=Object.entries(S.expCounts).reduce((a,[k,v])=>a+(_iamKeys.has(k)?0:(v||0)),0);
|
||||
const kpiCats=EXP_GROUPS.map(g=>{const cnt=g.tabs.reduce((a,t)=>a+(S.expCounts[t.key]||0),0);const detail=g.tabs.filter(t=>(S.expCounts[t.key]||0)>0).map(t=>t.label+': '+S.expCounts[t.key]).join('\n');return{name:g.name,icon:catIcons[g.name]||'',count:cnt,detail}}).filter(k=>k.count>0);
|
||||
const totalEl=document.getElementById('exp-kpi-total');if(totalEl)totalEl.textContent=totalRes;
|
||||
const subEl=document.getElementById('exp-kpi-sub');if(subEl)subEl.innerHTML=S.expCountsRefreshing?'<span class="spinner" style="width:10px;height:10px;display:inline-block;vertical-align:-1px"></span> atualizando...':'recursos';
|
||||
const catsEl=document.getElementById('exp-kpi-cats');if(catsEl)catsEl.innerHTML=kpiCats.slice(0,6).map(k=>`<div class="kpi" style="padding:.5rem .7rem;min-width:75px;text-align:center;flex:0 0 auto;cursor:default" title="${k.detail}"><div class="kpi-label" style="justify-content:center">${k.icon}</div><div class="kpi-val" style="font-size:1.2rem">${k.count}</div><div class="kpi-sub">${k.name}</div></div>`).join('');
|
||||
// Update tab counts
|
||||
const activeGrp=EXP_GROUPS.find(g=>g.name===S.expCat)||EXP_GROUPS[0];
|
||||
activeGrp.tabs.forEach(t=>{const te=document.getElementById('exp-tab-cnt-'+t.key);if(te)te.textContent=S.expCounts[t.key]!=null?'('+S.expCounts[t.key]+')':''});
|
||||
}
|
||||
async function expRefreshCounts(){
|
||||
const comp=S.expSelComp;const cfg=S.expCfg;if(!comp||!cfg)return;
|
||||
const myRun=++_expCountAbort;
|
||||
S.expCountsRefreshing=true;R();
|
||||
S.expCountsRefreshing=true;expUpdateKpi();
|
||||
try{
|
||||
await $api('/oci/explore/'+cfg+'/counts/refresh',{method:'POST',body:{compartment_id:comp,regions:S.expSelRegions}});
|
||||
const poll=async(attempts)=>{
|
||||
if(_expCountAbort!==myRun||S.expSelComp!==comp){S.expCountsRefreshing=false;R();return}
|
||||
if(attempts<=0){S.expCountsRefreshing=false;R();return}
|
||||
if(_expCountAbort!==myRun||S.expSelComp!==comp){S.expCountsRefreshing=false;expUpdateKpi();return}
|
||||
if(attempts<=0){S.expCountsRefreshing=false;expUpdateKpi();return}
|
||||
await new Promise(r=>setTimeout(r,3000));
|
||||
if(_expCountAbort!==myRun||S.expSelComp!==comp){S.expCountsRefreshing=false;R();return}
|
||||
await expLoadCachedCounts();
|
||||
if(_expCountAbort!==myRun||S.expSelComp!==comp){S.expCountsRefreshing=false;expUpdateKpi();return}
|
||||
await expLoadCachedCounts(true);
|
||||
poll(attempts-1);
|
||||
};
|
||||
poll(12);
|
||||
}catch(e){S.expCountsRefreshing=false;R();console.warn('Refresh counts error:',e)}
|
||||
}catch(e){S.expCountsRefreshing=false;expUpdateKpi();console.warn('Refresh counts error:',e)}
|
||||
}
|
||||
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>';
|
||||
@@ -2547,9 +2844,9 @@ function dlRpt(id,f){window.open(API+'/reports/'+id+'/download?fmt='+f+'&token='
|
||||
async function refreshDl(){S.reports=await $api('/reports');S.reportFiles={};S.dlExpandedRid=null;R()}
|
||||
|
||||
/* ── Edit helpers ── */
|
||||
function editCfg(type,id){S.editing={type,id};if(type==='oci'){const c=S.ociCfg.find(x=>x.id===id);S.ociFormRegVal=c?c.region:'';S.ociFormRegFilter=''}R();
|
||||
function editCfg(type,id){S.editing={type,id};if(type==='oci'){const c=S.ociCfg.find(x=>x.id===id);S.ociFormRegVal=c?c.region:'';S.ociFormRegFilter='';S.ociFormOpen=true;S.ociAuthType=c?.auth_type||'api_key'}R();
|
||||
const form=document.querySelector('.cd:has(#obtn), .cd:has(#gbtn), .cd:has(#abtn), .cd:has(#mcbtn)');if(form)form.scrollIntoView({behavior:'smooth',block:'start'})}
|
||||
function cancelEdit(){S.editing=null;S.ociFormRegVal='';S.ociFormRegFilter='';R()}
|
||||
function cancelEdit(){S.editing=null;S.ociFormRegVal='';S.ociFormRegFilter='';S.ociFormOpen=false;S.ociAuthType='api_key';R()}
|
||||
|
||||
/* ── OCI Config ── */
|
||||
function rOciRegionDd(eo){
|
||||
@@ -2569,6 +2866,10 @@ ${ddItems||'<div style="padding:.5rem;color:var(--t4);font-size:.72rem">Nenhuma
|
||||
</div>`:''}</div>`}
|
||||
function ociFormPickReg(r){S.ociFormRegVal=r;S.ociFormRegOpen=false;S.ociFormRegFilter='';R()}
|
||||
function rOci(){const eo=S.editing?.type==='oci'?S.ociCfg.find(c=>c.id===S.editing.id):null;
|
||||
const formOpen=S.ociFormOpen||!!eo;
|
||||
const at=eo?(eo.auth_type||'api_key'):S.ociAuthType;
|
||||
const isToken=at==='session_token';
|
||||
const authTag=t=>(t||'api_key')==='session_token'?'<span class="tag" style="background:#e67e2222;color:#e67e22;font-size:.58rem">Session Token</span>':'<span class="tag" style="background:var(--gnl);color:var(--gn);font-size:.58rem">API Key</span>';
|
||||
return`<div class="cd" style="display:flex;align-items:center;justify-content:space-between;padding:.6rem 1rem">
|
||||
<div style="font-size:.78rem;font-weight:600;color:var(--t2)">${IC.settings} Configuração do Sistema</div>
|
||||
<div style="display:flex;gap:.4rem;align-items:center">
|
||||
@@ -2578,44 +2879,71 @@ function rOci(){const eo=S.editing?.type==='oci'?S.ociCfg.find(c=>c.id===S.editi
|
||||
</div></div>
|
||||
<div id="cfgMsg"></div>
|
||||
<div class="cd"><div class="ct">${IC.cloud} Credenciais Registradas</div><div id="ocm"></div>
|
||||
<table><thead><tr><th>Tenancy</th><th>Region</th><th>Detalhes</th><th>Ações</th></tr></thead><tbody>
|
||||
${S.ociCfg.map(c=>`<tr${eo&&eo.id===c.id?' style="background:var(--bg3)"':''}><td><strong>${c.tenancy_name}</strong><br><span style="font-size:.66rem;color:var(--t4)">${c.created_at}</span></td><td><span class="tag">${c.region}</span></td>
|
||||
<td style="font-family:var(--fm);font-size:.66rem;color:var(--t4);line-height:1.6"><span title="Fingerprint">${IC.key} ••••••••••••••••••••</span><br><span title="Tenancy OCID">${IC.building} ${c.tenancy_ocid}</span><br><span title="User OCID">${IC.user} ${c.user_ocid}</span><br><span title="Compartment">${IC.pkg} ${c.compartment_id||'—'}</span><br><span title="SSH Public Key">${IC.key} SSH: ${c.ssh_public_key_preview||'<em>não configurada</em>'}</span></td>
|
||||
<table><thead><tr><th>Tenancy</th><th>Tipo</th><th>Region</th><th>Detalhes</th><th>Ações</th></tr></thead><tbody>
|
||||
${S.ociCfg.map(c=>`<tr${eo&&eo.id===c.id?' style="background:var(--bg3)"':''}><td><strong>${c.tenancy_name}</strong><br><span style="font-size:.66rem;color:var(--t4)">${c.created_at}</span></td><td>${authTag(c.auth_type)}</td><td><span class="tag">${c.region}</span></td>
|
||||
<td style="font-family:var(--fm);font-size:.66rem;color:var(--t4);line-height:1.6">${(c.auth_type||'api_key')==='session_token'?`<span title="Tenancy OCID">${IC.building} ${c.tenancy_ocid}</span><br><span title="Auth">${IC.lock} Session Token</span>`:`<span title="Fingerprint">${IC.key} ••••••••••••••••••••</span><br><span title="Tenancy OCID">${IC.building} ${c.tenancy_ocid}</span><br><span title="User OCID">${IC.user} ${c.user_ocid}</span>`}<br><span title="Compartment">${IC.pkg} ${c.compartment_id||'—'}</span><br><span title="SSH Public Key">${IC.key} SSH: ${c.ssh_public_key_preview||'<em>não configurada</em>'}</span></td>
|
||||
<td><button class="btn bs bsm" onclick="editCfg('oci','${c.id}')">Editar</button> <button class="btn bs bsm" onclick="tOci('${c.id}')">Testar</button> <button class="btn bd bsm" onclick="dOci('${c.id}')">Excluir</button></td></tr>`).join('')}</tbody></table>
|
||||
${!S.ociCfg.length?'<div class="emp" style="padding:.85rem"><p>Nenhuma credencial registrada.</p></div>':''}</div>
|
||||
<div class="cd"><div class="ct">${eo?IC.edit+' Editar Credencial':IC.plus+' Nova Credencial'}</div>
|
||||
<div class="cdesc">${eo?'Editando: <strong>'+eo.tenancy_name+'</strong>':'Adicione as credenciais da sua conta OCI para executar reports e acessar serviços.'}</div><div id="om"></div>
|
||||
<div class="cd">
|
||||
<div style="display:flex;align-items:center;cursor:pointer;gap:.5rem" onclick="${eo?'':`S.ociFormOpen=!S.ociFormOpen;R()`}">
|
||||
<svg viewBox="0 0 16 16" width="14" height="14" fill="var(--t3)" style="transition:transform .2s;transform:rotate(${formOpen?'90':'0'}deg)"><path d="M6 3l5 5-5 5z"/></svg>
|
||||
<div class="ct" style="margin:0">${eo?IC.edit+' Editar Credencial':IC.plus+' Nova Credencial'}</div>
|
||||
</div>
|
||||
${formOpen?`
|
||||
<div class="cdesc" style="margin-top:.5rem">${eo?'Editando: <strong>'+eo.tenancy_name+'</strong>':'Adicione as credenciais da sua conta OCI para executar reports e acessar serviços.'}</div><div id="om"></div>
|
||||
<div style="display:flex;gap:.5rem;margin-bottom:.7rem">
|
||||
<button class="tws-filter-btn${!isToken?' on':''}" onclick="S.ociAuthType='api_key';R()" ${eo?'disabled':''}>${IC.key} API Key</button>
|
||||
<button class="tws-filter-btn${isToken?' on':''}" onclick="S.ociAuthType='session_token';R()" ${eo?'disabled':''}>${IC.lock} Session Token</button>
|
||||
${eo?'<span style="font-size:.62rem;color:var(--t4);align-self:center">(tipo não pode ser alterado)</span>':''}
|
||||
</div>
|
||||
<div class="g2">
|
||||
<div class="ig"><label>Tenancy Name</label><input type="text" id="otn" placeholder="minha-empresa" value="${eo?eo.tenancy_name:''}"></div>
|
||||
<div class="ig"><label>OCID User</label><input type="password" id="ouo" placeholder="ocid1.user.oc1.." value="${eo?'':''}" autocomplete="off">${eo?'<div class="ht">Preencha para alterar. Atual: <code>'+eo.user_ocid+'</code></div>':''}</div>
|
||||
<div class="ig"><label>Fingerprint</label><input type="password" id="ofp" placeholder="aa:bb:cc:dd:..." value="${eo?'':''}" autocomplete="off">${eo?'<div class="ht">Preencha para alterar. Atual: <code>'+eo.fingerprint+'</code></div>':''}</div>
|
||||
<div class="ig"><label>OCID Tenancy</label><input type="password" id="oto" placeholder="ocid1.tenancy.oc1.." value="${eo?'':''}" autocomplete="off">${eo?'<div class="ht">Preencha para alterar. Atual: <code>'+eo.tenancy_ocid+'</code></div>':''}</div>
|
||||
<div class="ig"><label>Region</label>${rOciRegionDd(eo)}</div>
|
||||
<div class="ig"><label>Compartment OCID</label><input type="password" id="ocp" placeholder="ocid1.compartment.oc1.." value="${eo?'':''}" autocomplete="off">${eo?'<div class="ht">Preencha para alterar. Atual: <code>'+(eo.compartment_id||'—')+'</code></div>':''}</div></div>
|
||||
<div class="g2"><div class="ig"><label>Private Key (.pem)</label>${eo?'<div class="ht">Deixe vazio para manter a chave atual</div>':''}<input type="file" id="osk" accept=".pem"></div>
|
||||
<div class="ig"><label>Public Key (.pem)</label><input type="file" id="opk" accept=".pem"></div></div>
|
||||
<div class="g2"><div class="ig"><label>Key Passphrase</label><div class="ht">Apenas se a chave privada for protegida por senha</div><input type="password" id="okp" placeholder="(opcional)"></div>
|
||||
${!isToken?`<div class="g2">
|
||||
<div class="ig"><label>OCID User</label><input type="password" id="ouo" placeholder="ocid1.user.oc1.." value="" autocomplete="off">${eo?'<div class="ht">Preencha para alterar. Atual: <code>'+eo.user_ocid+'</code></div>':''}</div>
|
||||
<div class="ig"><label>Fingerprint</label><input type="password" id="ofp" placeholder="aa:bb:cc:dd:..." value="" autocomplete="off">${eo?'<div class="ht">Preencha para alterar. Atual: <code>'+eo.fingerprint+'</code></div>':''}</div></div>`
|
||||
:`<div class="g2">
|
||||
<div class="ig"><label>Session Token</label><div class="ht">Cole o conteúdo do arquivo token gerado por <code>oci session authenticate</code></div><textarea id="ostk" rows="3" placeholder="Conteúdo do arquivo ~/.oci/sessions/DEFAULT/token" style="font-size:.68rem;font-family:var(--fm);resize:vertical"></textarea></div>
|
||||
<div class="ig"><label>OCID User <span style="font-size:.6rem;color:var(--t4)">(opcional)</span></label><input type="password" id="ouo" placeholder="ocid1.user.oc1.. (preenchido automaticamente)" value="" autocomplete="off"><div class="ht">O SDK pode resolver o user a partir do token. Deixe vazio se preferir.</div></div></div>`}
|
||||
<div class="g2"><div class="ig"><label>${isToken?'Chave de Sessão (.pem)':'Private Key (.pem)'}</label>${eo?'<div class="ht">Deixe vazio para manter a chave atual</div>':''}<div class="ht">${isToken?'Arquivo <code>oci_api_key.pem</code> da pasta de sessão':'Chave privada da API Key OCI'}</div><input type="file" id="osk" accept=".pem"></div>
|
||||
${!isToken?'<div class="ig"><label>Public Key (.pem)</label><input type="file" id="opk" accept=".pem"></div>':'<div class="ig"></div>'}</div>
|
||||
<div class="g2">${!isToken?'<div class="ig"><label>Key Passphrase</label><div class="ht">Apenas se a chave privada for protegida por senha</div><input type="password" id="okp" placeholder="(opcional)"></div>':'<div class="ig"></div>'}
|
||||
<div class="ig"><label>SSH Public Key</label><div class="ht">Chave SSH para acesso a instâncias compute (ex: ssh-rsa AAAA...)</div><textarea id="osshk" rows="2" placeholder="ssh-rsa AAAAB3NzaC1yc2E... (opcional)" style="font-size:.72rem;font-family:var(--fm);resize:vertical">${eo&&eo.ssh_public_key?eo.ssh_public_key:''}</textarea>${eo&&eo.ssh_public_key_preview?'<div class="ht">Atual: <code>'+eo.ssh_public_key_preview+'</code></div>':''}</div></div>
|
||||
<div style="display:flex;gap:8px"><button class="btn bp" id="obtn" onclick="sOci()">${eo?'Salvar Alterações':'Salvar Credencial'}</button>${eo?'<button class="btn bs" onclick="cancelEdit()">Cancelar</button>':''}</div></div>
|
||||
<div style="display:flex;gap:8px"><button class="btn bp" id="obtn" onclick="sOci()">${eo?'Salvar Alterações':'Salvar Credencial'}</button>${eo?'<button class="btn bs" onclick="cancelEdit()">Cancelar</button>':''}</div>
|
||||
`:''}
|
||||
</div>
|
||||
`+rConfigLogs('oci')}
|
||||
async function sOci(){const tn=document.getElementById('otn').value;
|
||||
if(!tn)return sm('om','Preencha o Tenancy Name','e');
|
||||
const fd=new FormData();fd.append('tenancy_name',tn);fd.append('tenancy_ocid',document.getElementById('oto').value);
|
||||
fd.append('user_ocid',document.getElementById('ouo').value);fd.append('fingerprint',document.getElementById('ofp').value);
|
||||
fd.append('region',S.ociFormRegVal||'');fd.append('compartment_id',document.getElementById('ocp').value);
|
||||
fd.append('key_passphrase',document.getElementById('okp').value||'');
|
||||
fd.append('ssh_public_key',document.getElementById('osshk').value||'');
|
||||
const sk=document.getElementById('osk').files[0];
|
||||
const isEdit=S.editing?.type==='oci';
|
||||
const eo=isEdit?S.ociCfg.find(c=>c.id===S.editing.id):null;
|
||||
const at=eo?(eo.auth_type||'api_key'):S.ociAuthType;
|
||||
const isToken=at==='session_token';
|
||||
const fd=new FormData();fd.append('tenancy_name',tn);fd.append('tenancy_ocid',document.getElementById('oto').value);
|
||||
fd.append('user_ocid',(document.getElementById('ouo')||{}).value||'');
|
||||
fd.append('fingerprint',(document.getElementById('ofp')||{}).value||'');
|
||||
fd.append('region',S.ociFormRegVal||'');fd.append('compartment_id',document.getElementById('ocp').value);
|
||||
fd.append('key_passphrase',(document.getElementById('okp')||{}).value||'');
|
||||
fd.append('ssh_public_key',document.getElementById('osshk').value||'');
|
||||
fd.append('auth_type',at);
|
||||
fd.append('security_token',(document.getElementById('ostk')||{}).value||'');
|
||||
const sk=document.getElementById('osk').files[0];
|
||||
if(!isEdit){
|
||||
if(!document.getElementById('oto').value||!document.getElementById('ouo').value||!document.getElementById('ofp').value)return sm('om','Preencha OCID Tenancy, OCID User e Fingerprint','e');
|
||||
if(!sk)return sm('om','Selecione a chave privada','e');
|
||||
if(!document.getElementById('oto').value)return sm('om','Preencha o OCID Tenancy','e');
|
||||
if(!isToken){
|
||||
if(!document.getElementById('ouo').value||!document.getElementById('ofp').value)return sm('om','Preencha OCID User e Fingerprint','e');
|
||||
}else{
|
||||
if(!(document.getElementById('ostk')||{}).value)return sm('om','Cole o Session Token','e');
|
||||
}
|
||||
if(!sk)return sm('om',isToken?'Selecione a chave de sessão (.pem)':'Selecione a chave privada (.pem)','e');
|
||||
}
|
||||
if(sk)fd.append('private_key',sk);
|
||||
const pk=document.getElementById('opk').files[0];if(pk)fd.append('public_key',pk);
|
||||
const pk=(document.getElementById('opk')||{}).files?.[0];if(pk)fd.append('public_key',pk);
|
||||
const btn=document.getElementById('obtn');btn.disabled=true;btn.textContent='Salvando...';sm('om','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Salvando credencial...','i');
|
||||
try{const url=isEdit?'/oci/configs/'+S.editing.id:'/oci/config';const method=isEdit?'PUT':'POST';
|
||||
await $api(url,{method,body:fd,headers:{}});S.editing=null;S.ociFormRegVal='';S.ociFormRegFilter='';S.ociCfg=await $api('/oci/configs');sm('om',IC.ok+' Credencial '+(isEdit?'atualizada':'salva')+' com sucesso!','s');R()}catch(e){sm('om',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Salvar Credencial'}}
|
||||
await $api(url,{method,body:fd,headers:{}});S.editing=null;S.ociFormRegVal='';S.ociFormRegFilter='';S.ociFormOpen=false;S.ociAuthType='api_key';S.ociCfg=await $api('/oci/configs');sm('om',IC.ok+' Credencial '+(isEdit?'atualizada':'salva')+' com sucesso!','s');R()}catch(e){sm('om',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Salvar Credencial'}}
|
||||
async function cfgExport(){
|
||||
try{
|
||||
const resp=await fetch(API+'/config/export',{headers:S.token?{'Authorization':'Bearer '+S.token}:{}});
|
||||
|
||||
Reference in New Issue
Block a user