feat: Terraform Agent, expanded OCI Explorer (40+ resources), and MCP improvements
Add Terraform Agent tab with AI-powered IaC generation (plan/apply/destroy lifecycle, workspace management, compartment selection, Terraform CLI v1.7.5 in container). Expand OCI Explorer from 6 to 40+ resource types across 8 categories with tree-view navigation and resizable panels. Add compartment filtering and error tracking to MCP CIS server, increase tool timeout to 30min with auto-retry. Add chat sidebar, chat audit logs, and tune memory compaction (6K threshold, 20 recent messages). Trim model catalog from 69 to 15 curated models. Update README for v2.1.
This commit is contained in:
@@ -115,10 +115,22 @@ tbody tr:last-child td{border-bottom:none}
|
||||
.g3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:.75rem}
|
||||
|
||||
/* ── Chat ── */
|
||||
.ch-c{display:flex;flex-direction:column;height:calc(100vh - 56px - 3rem);overflow:hidden}
|
||||
.ch-t{display:flex;gap:.5rem;align-items:center;padding:.6rem .85rem;background:var(--bg1);border:1px solid var(--bd);
|
||||
border-radius:var(--rl) var(--rl) 0 0;font-size:.74rem;flex-wrap:wrap;box-shadow:var(--sh1)}
|
||||
.ch-c{display:flex;height:calc(100vh - 56px - 3rem);overflow:hidden;border:1px solid var(--bd);border-radius:var(--rl);box-shadow:var(--sh2)}
|
||||
.ch-side{width:0;overflow:hidden;transition:width .3s cubic-bezier(.4,0,.2,1);background:var(--bg1);border-left:0 solid var(--bd);flex-shrink:0;order:2}
|
||||
.ch-side.open{width:300px;border-left-width:1px}
|
||||
.ch-side-inner{width:300px;padding:.85rem;overflow-y:auto;height:100%;box-sizing:border-box}
|
||||
.ch-side-title{font-weight:700;font-size:.82rem;padding-bottom:.5rem;border-bottom:1px solid var(--bd);margin-bottom:.65rem;display:flex;align-items:center;gap:.4rem}
|
||||
.ch-side-section{margin-bottom:.85rem}
|
||||
.ch-side-section label{font-weight:600;color:var(--t3);font-size:.68rem;display:block;margin-bottom:3px}
|
||||
.ch-side-section input[type="number"],.ch-side-section select{width:100%;padding:.32rem .5rem;font-size:.74rem;border:1px solid var(--bd);border-radius:6px;background:var(--bg2);box-sizing:border-box}
|
||||
.ch-side .g2{display:grid;grid-template-columns:1fr 1fr;gap:.5rem}
|
||||
.ch-main{flex:1;display:flex;flex-direction:column;min-width:0;order:1}
|
||||
.ch-t{display:flex;gap:.5rem;align-items:center;padding:.55rem .85rem;background:var(--bg1);
|
||||
border-bottom:1px solid var(--bd);font-size:.74rem;flex-wrap:wrap}
|
||||
.ch-t select{max-width:260px;font-size:.74rem;padding:.35rem .5rem}
|
||||
.ch-icon-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:8px;cursor:pointer;border:1px solid var(--bd);background:var(--bg1);font-size:.82rem;transition:all .2s;flex-shrink:0}
|
||||
.ch-icon-btn:hover{border-color:var(--ac);color:var(--ac)}
|
||||
.ch-icon-btn.active{background:var(--pl);border-color:var(--ac);color:var(--ac)}
|
||||
.mdrop{position:relative;display:inline-block}
|
||||
.mdrop-btn{background:var(--bg2);border:1px solid var(--bd);border-radius:8px;padding:.35rem .6rem;font-size:.74rem;cursor:pointer;max-width:320px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:center;gap:.3rem;min-width:180px}
|
||||
.mdrop-btn:hover{border-color:var(--ac)}
|
||||
@@ -131,14 +143,8 @@ tbody tr:last-child td{border-bottom:none}
|
||||
.mdrop-list .itm{padding:.3rem .7rem;font-size:.74rem;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.mdrop-list .itm:hover{background:var(--bg3)}
|
||||
.mdrop-list .itm.sel{background:var(--pl);color:var(--p);font-weight:600}
|
||||
.ch-params{padding:.55rem .85rem;background:var(--bg1);border-left:1px solid var(--bd);border-right:1px solid var(--bd);border-bottom:1px solid var(--bg3);font-size:.72rem}
|
||||
.ch-params .g3{display:grid;grid-template-columns:repeat(3,1fr);gap:.5rem;margin-bottom:.3rem}
|
||||
.ch-params label{font-weight:600;color:var(--t3);font-size:.66rem;margin-bottom:2px;display:block}
|
||||
.ch-params input{width:100%;padding:.25rem .4rem;font-size:.72rem;border:1px solid var(--bd);border-radius:6px;background:var(--bg2)}
|
||||
.gear-btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border-radius:8px;cursor:pointer;border:1px solid var(--bd);background:var(--bg1);font-size:.78rem;transition:all .2s;flex-shrink:0}
|
||||
.gear-btn:hover{border-color:var(--ac);color:var(--ac)}
|
||||
.ch-t label{font-weight:600;color:var(--t3);font-size:.72rem}
|
||||
.ch-m{flex:1;overflow-y:auto;padding:1rem;background:var(--bg2);border-left:1px solid var(--bd);border-right:1px solid var(--bd)}
|
||||
.ch-m{flex:1;overflow-y:auto;padding:1rem;background:var(--bg2)}
|
||||
.cm{margin-bottom:.85rem;display:flex;gap:.6rem;animation:msgIn .3s var(--trans)}.cm-user{justify-content:flex-end}
|
||||
@keyframes msgIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
||||
.cm-user .cb{background:linear-gradient(145deg,var(--ac),var(--ach));color:#fff;border-radius:var(--rl) var(--rl) 6px var(--rl);border-color:transparent;box-shadow:0 2px 8px rgba(199,70,52,.15)}
|
||||
@@ -149,8 +155,7 @@ tbody tr:last-child td{border-bottom:none}
|
||||
.cm-ts{font-size:.65rem;color:#999;margin-top:3px;opacity:.7}
|
||||
.cb code{font-family:var(--fm);font-size:.73rem;background:var(--bg3);padding:.12rem .3rem;border-radius:5px}
|
||||
.cb strong{color:var(--ac)}
|
||||
.ch-i{display:flex;gap:.45rem;padding:.7rem .85rem;background:var(--bg1);border:1px solid var(--bd);border-top:none;
|
||||
border-radius:0 0 var(--rl) var(--rl);box-shadow:0 -2px 8px rgba(0,0,0,.02)}
|
||||
.ch-i{display:flex;gap:.45rem;padding:.7rem .85rem;background:var(--bg1);border-top:1px solid var(--bd);box-shadow:0 -2px 8px rgba(0,0,0,.02)}
|
||||
.ch-i input{flex:1;background:var(--bg2);border-radius:10px}
|
||||
.ch-i .btn{border-radius:10px}
|
||||
|
||||
@@ -185,11 +190,36 @@ tbody tr:last-child td{border-bottom:none}
|
||||
.emp p{line-height:1.55}
|
||||
|
||||
/* ── Explorer ── */
|
||||
.exp-r{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:.65rem;margin-top:.65rem}
|
||||
.exp-c{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);padding:.85rem;font-size:.78rem;transition:all .2s var(--trans)}
|
||||
.exp-wrap{display:flex;height:calc(100vh - 56px - 3rem);border:1px solid var(--bd);border-radius:var(--rl);overflow:hidden;box-shadow:var(--sh2)}
|
||||
.exp-tree{width:240px;flex-shrink:0;background:var(--bg1);border-right:none;overflow-y:auto;padding:.5rem 0;min-width:140px;max-width:50%}
|
||||
.exp-resize{width:5px;flex-shrink:0;cursor:col-resize;background:var(--bd);transition:background .15s;position:relative}
|
||||
.exp-resize:hover,.exp-resize.dragging{background:var(--ac);opacity:.6}
|
||||
.exp-resize::after{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:3px;height:28px;border-left:1px solid var(--t4);border-right:1px solid var(--t4);opacity:.4}
|
||||
.exp-resize:hover::after,.exp-resize.dragging::after{border-color:var(--ac);opacity:.8}
|
||||
.exp-main{flex:1;display:flex;flex-direction:column;min-width:0;overflow:hidden}
|
||||
.exp-toolbar{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border-bottom:1px solid var(--bd);background:var(--bg);flex-wrap:wrap}
|
||||
.exp-toolbar select{font-size:.74rem;padding:.3rem .5rem;border-radius:8px;border:1px solid var(--bd);background:var(--bg2);color:var(--t1)}
|
||||
.exp-nav{display:flex;border-bottom:1px solid var(--bd);background:var(--bg);flex-shrink:0}
|
||||
.exp-cats{display:flex;overflow-x:auto;gap:0;flex:1}
|
||||
.exp-cat{padding:.45rem .7rem;font-size:.66rem;font-weight:600;cursor:pointer;color:var(--t4);white-space:nowrap;border-bottom:2px solid transparent;transition:all .15s;text-transform:uppercase;letter-spacing:.3px}
|
||||
.exp-cat:hover{color:var(--t2);background:var(--bg2)}
|
||||
.exp-cat.active{color:var(--ac);border-bottom-color:var(--ac);background:var(--acl)}
|
||||
.exp-tabs{display:flex;gap:3px;padding:.3rem .75rem;border-bottom:1px solid var(--bd);background:var(--bg2);overflow-x:auto;flex-shrink:0}
|
||||
.exp-tab{padding:.3rem .6rem;font-size:.7rem;border-radius:8px;cursor:pointer;color:var(--t3);white-space:nowrap;transition:all .15s;border:1px solid transparent}
|
||||
.exp-tab:hover{background:var(--bg1);color:var(--t1);border-color:var(--bd)}
|
||||
.exp-tab.active{background:var(--bg1);color:var(--ac);font-weight:600;border-color:var(--ac);box-shadow:0 1px 3px rgba(199,70,52,.1)}
|
||||
.exp-content{flex:1;overflow-y:auto;padding:.75rem}
|
||||
.exp-r{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:.65rem}
|
||||
.exp-c{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);padding:.85rem;font-size:.78rem;transition:all .2s var(--trans);overflow:hidden;word-wrap:break-word;overflow-wrap:break-word;min-width:0;box-sizing:border-box}
|
||||
.exp-c:hover{border-color:var(--bdf);box-shadow:var(--sh1);transform:translateY(-1px)}
|
||||
.exp-c strong{color:var(--t1);font-size:.84rem;font-weight:600}
|
||||
.exp-c .em{font-family:var(--fm);font-size:.65rem;color:var(--t4);word-break:break-all}
|
||||
.exp-c strong{color:var(--t1);font-size:.84rem;font-weight:600;display:block;overflow:hidden;text-overflow:ellipsis}
|
||||
.exp-c .em{font-family:var(--fm);font-size:.65rem;color:var(--t4);word-break:break-all;overflow-wrap:break-word;display:inline-block;max-width:100%}
|
||||
.exp-node{padding:.15rem 0;font-size:.74rem;color:var(--t2)}
|
||||
.exp-node-row{display:flex;align-items:center;gap:.25rem;padding:.25rem .5rem;border-radius:6px;cursor:pointer;transition:all .15s}
|
||||
.exp-node-row:hover{background:var(--bg2)}
|
||||
.exp-node-row.selected{background:var(--acl);color:var(--ac);font-weight:600;border-left:2px solid var(--ac)}
|
||||
.exp-node-toggle{width:16px;text-align:center;font-size:.55rem;color:var(--t4);flex-shrink:0;cursor:pointer;transition:transform .15s}
|
||||
.exp-node-children{padding-left:.85rem;border-left:1px solid var(--bg4);margin-left:.65rem}
|
||||
|
||||
/* ── Animations ── */
|
||||
@keyframes fi{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
|
||||
@@ -206,7 +236,70 @@ tbody tr:last-child td{border-bottom:none}
|
||||
::-webkit-scrollbar-thumb:hover{background:var(--bdf)}
|
||||
|
||||
/* ── Responsive ── */
|
||||
@media(max-width:768px){.sb{transform:translateX(-100%)}.sb.open{transform:translateX(0)}.mc{margin-left:0}.g2,.g3{grid-template-columns:1fr}.pc{padding:1rem}}
|
||||
@media(max-width:768px){.sb{transform:translateX(-100%)}.sb.open{transform:translateX(0)}.mc{margin-left:0}.g2,.g3{grid-template-columns:1fr}.pc{padding:1rem}
|
||||
.exp-wrap{flex-direction:column;height:auto;min-height:calc(100vh - 56px - 3rem)}
|
||||
.exp-tree{width:100%;max-height:180px;border-right:none;border-bottom:1px solid var(--bd)}
|
||||
.exp-r{grid-template-columns:1fr}
|
||||
.exp-cats{flex-wrap:wrap}
|
||||
.exp-cat{padding:.3rem .5rem;font-size:.6rem}
|
||||
}
|
||||
/* ── Terraform Agent ── */
|
||||
.tf-wrap{display:flex;flex-direction:column;height:calc(100vh - 56px)}
|
||||
.tf-chat{flex:1;display:flex;flex-direction:column;overflow:hidden;min-height:200px}
|
||||
.tf-toolbar{display:flex;align-items:center;gap:.5rem;padding:.45rem .7rem;border-bottom:1px solid var(--bd);flex-wrap:wrap;background:var(--bg)}
|
||||
.tf-msgs{flex:1;overflow-y:auto;padding:.7rem}
|
||||
.tf-input{display:flex;gap:.5rem;padding:.5rem .7rem;border-top:1px solid var(--bd)}
|
||||
.tf-input input{flex:1}
|
||||
.tf-resize{height:5px;cursor:row-resize;background:var(--bd);transition:background .15s;position:relative;flex-shrink:0}
|
||||
.tf-resize:hover,.tf-resize.dragging{background:#7b42bc;opacity:.7}
|
||||
.tf-resize::after{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:28px;height:3px;border-top:1px solid var(--t4);border-bottom:1px solid var(--t4);opacity:.4}
|
||||
.tf-resize:hover::after{border-color:#7b42bc;opacity:.8}
|
||||
.tf-bottom{height:280px;display:flex;flex-direction:column;overflow:hidden;border-top:1px solid var(--bd);background:var(--bg1)}
|
||||
.tf-bottom-tabs{display:flex;align-items:center;gap:0;border-bottom:1px solid var(--bd);background:var(--bg);padding:0 .5rem}
|
||||
.tf-btab{padding:.4rem .75rem;font-size:.72rem;font-weight:600;cursor:pointer;border-bottom:2px solid transparent;color:var(--t3);display:flex;align-items:center;gap:.3rem;transition:all .15s}
|
||||
.tf-btab:hover{color:var(--t1)}
|
||||
.tf-btab.active{color:#7b42bc;border-bottom-color:#7b42bc}
|
||||
.tf-btab svg{width:14px;height:14px}
|
||||
.tf-bottom-actions{margin-left:auto;display:flex;align-items:center;gap:.4rem;padding-right:.3rem}
|
||||
.tf-bottom-content{flex:1;overflow:auto;padding:.5rem .7rem}
|
||||
.tf-file{display:flex;align-items:center;gap:.5rem;padding:.45rem .6rem;border-radius:8px;border:1px solid var(--bd);margin-bottom:.4rem;background:var(--bg);transition:border-color .15s}
|
||||
.tf-file:hover{border-color:#7b42bc}
|
||||
.tf-file-icon{width:32px;height:32px;border-radius:6px;background:linear-gradient(135deg,#7b42bc22,#5c4ee522);display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||
.tf-file-icon svg{width:16px;height:16px;fill:#7b42bc}
|
||||
.tf-file-info{flex:1;min-width:0}
|
||||
.tf-file-name{font-size:.76rem;font-weight:600;font-family:var(--fm);color:var(--t1)}
|
||||
.tf-file-meta{font-size:.64rem;color:var(--t3)}
|
||||
.tf-file-actions{display:flex;gap:.3rem}
|
||||
.tf-file-btn{width:26px;height:26px;border-radius:6px;border:1px solid var(--bd);background:var(--bg);display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:.7rem;transition:all .15s}
|
||||
.tf-file-btn:hover{border-color:#7b42bc;background:#7b42bc11}
|
||||
.tf-code-block{background:#1e1e2e;border-radius:var(--r);overflow:hidden;margin:.6rem 0}
|
||||
.tf-code-header{display:flex;align-items:center;gap:.4rem;padding:.4rem .7rem;background:#2a2a3e;font-size:.72rem;color:#cdd6f4;border-bottom:1px solid #45475a}
|
||||
.tf-code-header span{flex:1;font-weight:600}
|
||||
.tf-code-header .btn{color:#cdd6f4;border-color:#45475a;background:transparent;font-size:.66rem;padding:2px 8px}
|
||||
.tf-code-header .btn:hover{background:#45475a}
|
||||
.tf-pre{padding:.85rem;overflow-x:auto;font-size:.74rem;line-height:1.6;color:#cdd6f4;font-family:var(--fm);margin:0;white-space:pre}
|
||||
.tf-plan-block{background:var(--gnl);border:1px solid rgba(22,163,74,.15);border-radius:var(--r);margin:.6rem 0;overflow:hidden}
|
||||
.tf-plan-header{padding:.45rem .7rem;font-weight:700;font-size:.76rem;color:var(--gn);border-bottom:1px solid rgba(22,163,74,.12)}
|
||||
.tf-plan-list{padding:.4rem .7rem}
|
||||
.tf-plan-item{display:flex;align-items:baseline;gap:.5rem;padding:.25rem 0;font-size:.74rem}
|
||||
.tf-plan-add{color:var(--gn);font-weight:700;font-family:var(--fm)}
|
||||
.tf-plan-type{font-family:var(--fm);font-weight:600;color:var(--t1)}
|
||||
.tf-plan-desc{color:var(--t3);font-size:.68rem}
|
||||
.tf-output{background:#1a1a2e;color:#cdd6f4;font-family:var(--fm);font-size:.7rem;line-height:1.5;padding:.6rem;overflow:auto;white-space:pre-wrap;word-break:break-word;border-radius:var(--r);height:100%}
|
||||
.tf-badge{font-size:.58rem;padding:1px 6px;border-radius:10px;font-weight:700;display:inline-flex;align-items:center;gap:3px}
|
||||
.tf-badge-draft{background:#7b42bc18;color:#7b42bc}
|
||||
.tf-badge-ok{background:var(--gnl);color:var(--gn)}
|
||||
.tf-badge-run{background:var(--bll);color:var(--bl)}
|
||||
.tf-badge-err{background:var(--rdl);color:var(--rd)}
|
||||
.tf-badge-dest{background:var(--t4);color:var(--bg)}
|
||||
.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 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}
|
||||
@media(max-width:768px){
|
||||
.tf-bottom{height:200px}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -216,13 +309,16 @@ 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,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:{},editing:null,
|
||||
chatModel:'',chatOci:'',chatRegion:'',chatCompartment:'',
|
||||
chatParams:{temperature:1,max_tokens:6000,top_p:0.95,top_k:1,frequency_penalty:0,presence_penalty:0},chatParamsOpen:false,chatUseTools:true,
|
||||
chatParams:{temperature:1,max_tokens:6000,top_p:0.95,top_k:1,frequency_penalty:0,presence_penalty:0},chatPanel:'',chatUseTools:true,
|
||||
chatPrompts:[],editingPrompt:null,trackingReportId:null,ociRegions:{},rptSelRegions:[],rptRegionsOpen:false,rptRegionFilter:'',
|
||||
rptOciOpen:false,rptOciFilter:'',rptOciVal:'',rptRselOpen:false,rptRselFilter:'',rptRselVal:'',ociFormRegOpen:false,ociFormRegFilter:'',ociFormRegVal:'',
|
||||
rptLevel:2,rptObp:false,rptRaw:false,rptRedact:false,reportFiles:{},dlExpandedRid:null,dlTenancyFilter:'',
|
||||
cisVer:null,cisCheckResult:null,cisUpdating:false,rptHistFilter:'',chatFiles:[]};
|
||||
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',tfResources:null,tfResLoading:false};
|
||||
const API='/api';
|
||||
|
||||
async function $api(p,o={}){const h={...(o.headers||{})};if(S.token)h['Authorization']='Bearer '+S.token;
|
||||
@@ -241,11 +337,12 @@ async function loadData(){try{
|
||||
try{const m=await $api('/genai/models');S.models=m.models;S.regions=m.regions;S.embModels=m.embedding_models||{};S.ociRegions=m.oci_regions||{}}catch(e){}
|
||||
try{S.adbCfg=await $api('/adb/configs')}catch(e){S.adbCfg=[]}
|
||||
try{S.chatPrompts=await $api('/prompts/chat')}catch(e){S.chatPrompts=[]}
|
||||
if(!S.chatModel){const dc=S.genaiCfg.find(g=>g.is_default);if(dc)S.chatModel='cfg:'+dc.id;else if(S.models['openai.gpt-4.1'])S.chatModel='openai.gpt-4.1'}
|
||||
if(S.user.role==='admin'){try{S.users=await $api('/users')}catch(e){}}
|
||||
try{S.cisVer=await $api('/cis-engine/version')}catch(e){}
|
||||
}catch(e){console.error(e)}}
|
||||
function R(){document.getElementById('app').innerHTML=S.user?rApp():rLogin();if(S.editing?.type==='mcp')setTimeout(mcpTypeFields,0)}
|
||||
function switchTab(t){S.tab=t;S.expData=null;R();if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl();
|
||||
function switchTab(t){S.tab=t;R();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 ── */
|
||||
@@ -264,8 +361,9 @@ function rLogin(){return`<div class="lp"><div class="lc fi">
|
||||
/* ── App Shell ── */
|
||||
function rApp(){return`<div class="app">${rSb()}<div class="mc">${rTb()}<div class="pc fi" id="pg">${rPg()}</div></div></div>`}
|
||||
|
||||
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','💬','Chat Agent'],['explorer','🔍','OCI Explorer'],['report','📊','Reports'],['downloads','📁','Downloads']];
|
||||
const tabs=[['chat','💬','Chat Agent'],['terraform',TF_IC,'Terraform'],['explorer','🔍','OCI Explorer'],['report','📊','Reports'],['downloads','📁','Downloads']];
|
||||
const ctabs=[['oci-config','☁️','Credenciais OCI'],['genai','🧠','GenAI Config'],['mcp','🔌','MCP Servers'],['adb','🗄️','ADB Vector'],['embeddings','🧬','Embeddings']];
|
||||
const atabs=[['users','👥','Usuários'],['mfa','🔐','MFA'],['audit','📋','Audit Log']];
|
||||
const i=(S.user?.first_name||S.user?.username||'?')[0].toUpperCase();
|
||||
@@ -280,57 +378,70 @@ ${atabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[
|
||||
<div class="sb-f"><div class="ui"><div class="ua">${i}</div><div><div class="un">${S.user?.first_name?S.user.first_name+' '+S.user.last_name:S.user?.username}</div><div class="ur">${S.user?.role}</div></div>
|
||||
<div class="lo-btn" onclick="doLogout()" title="Sair">⏻</div></div></div></div>`}
|
||||
|
||||
function rTb(){const t={'chat':'💬 AI Agent Chat','explorer':'🔍 OCI Account Explorer','report':'📊 Compliance Reports','downloads':'📁 Downloads','oci-config':'☁️ Credenciais OCI','genai':'🧠 OCI Generative AI','mcp':'🔌 MCP Servers','adb':'🗄️ Autonomous DB Vector','embeddings':'🧬 Embeddings','users':'👥 Gerenciar Usuários','mfa':'🔐 Autenticação MFA','audit':'📋 Audit Log'};
|
||||
function rTb(){const t={'chat':'💬 AI Agent Chat','terraform':TF_IC+' Terraform Agent','explorer':'🔍 OCI Account Explorer','report':'📊 Compliance Reports','downloads':'📁 Downloads','oci-config':'☁️ Credenciais OCI','genai':'🧠 OCI Generative AI','mcp':'🔌 MCP Servers','adb':'🗄️ Autonomous DB Vector','embeddings':'🧬 Embeddings','users':'👥 Gerenciar Usuários','mfa':'🔐 Autenticação MFA','audit':'📋 Audit Log'};
|
||||
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'explorer':return rExplorer();case'report':return rReport();case'downloads':return rDl();case'oci-config':return rOci();case'genai':return rGenAI();case'mcp':return rMCP();case'adb':return rADB();case'embeddings':return rEmbeddings();case'users':return rUsers();case'mfa':return rMfa();case'audit':return rAudit();default:return''}}
|
||||
function rPg(){switch(S.tab){case'chat':return rChat();case'terraform':return rTerraform();case'explorer':return rExplorer();case'report':return rReport();case'downloads':return rDl();case'oci-config':return rOci();case'genai':return rGenAI();case'mcp':return rMCP();case'adb':return rADB();case'embeddings':return rEmbeddings();case'users':return rUsers();case'mfa':return rMfa();case'audit':return rAudit();default:return''}}
|
||||
|
||||
/* ── Chat ── */
|
||||
function rChat(){
|
||||
const ms=S.msgs.length===0?'<div class="emp"><div class="eic">🤖</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('');
|
||||
// Build custom dropdown items
|
||||
// Skip non-chat models: Codex (completions only), Image, Audio, Guard, ProtectAI, GPT-oss, Pro (Responses API only)
|
||||
const skip=new Set(['openai.gpt-image-1','openai.gpt-image-1.5','openai.gpt-audio','meta.llama-guard-4-12b','protectai.deberta-v3-base-prompt-injection-v2',
|
||||
'openai.gpt-5.3-codex','openai.gpt-5.2-codex','openai.gpt-5.1-codex','openai.gpt-5.1-codex-max','openai.gpt-5.1-codex-mini','openai.gpt-5-codex',
|
||||
'openai.gpt-oss-120b','openai.gpt-oss-20b']);
|
||||
const provs={};for(const[mid,info]of Object.entries(S.models)){if(skip.has(mid))continue;const p=info.provider||'other';if(!provs[p])provs[p]=[];provs[p].push({id:mid,name:info.name})}
|
||||
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='';
|
||||
if(S.genaiCfg.length){ddItems+=`<div class="grp">Configs Salvas</div>`;S.genaiCfg.forEach(g=>{const mi=S.models[g.model_id];ddItems+=`<div class="itm${S.chatModel==='cfg:'+g.id?' sel':''}" onclick="pickModel('cfg:${g.id}')">${g.name||mi?.name||g.model_id} (${g.genai_region})</div>`})}
|
||||
provOrder.filter(p=>provs[p]).forEach(p=>{ddItems+=`<div class="grp">${p.charAt(0).toUpperCase()+p.slice(1)}</div>`;provs[p].forEach(m=>{ddItems+=`<div class="itm${S.chatModel===m.id?' sel':''}" onclick="pickModel('${m.id}')">${m.name}</div>`})});
|
||||
// Current label
|
||||
let curLabel='Selecione um modelo...';
|
||||
if(S.chatModel.startsWith('cfg:')){const g=S.genaiCfg.find(x=>x.id===S.chatModel.slice(4));const mi=g?S.models[g.model_id]:null;curLabel=g?(g.name||mi?.name||g.model_id)+' ('+g.genai_region+')':'Config...'}
|
||||
else if(S.chatModel){const mi=S.models[S.chatModel];curLabel=mi?mi.name:S.chatModel}
|
||||
const isDirect=S.chatModel&&!S.chatModel.startsWith('cfg:');
|
||||
const hasModel=!!S.chatModel;
|
||||
// OCI selector for direct mode
|
||||
const ociSel=isDirect?`<select id="coci" onchange="chatOciChanged()" style="max-width:200px"><option value="">Credencial OCI...</option>${S.ociCfg.map(c=>`<option value="${c.id}" ${S.chatOci===c.id?'selected':''}>${c.tenancy_name} (${c.region})</option>`).join('')}</select>`:'';
|
||||
const gearBtn=hasModel?`<div class="gear-btn" onclick="toggleChatParams()" title="Parâmetros">${S.chatParamsOpen?'✕':'⚙️'}</div>`:'';
|
||||
const ragBadge=S.adbCfg.some(c=>c.genai_config_id&&c.is_active)?'<span class="tag" style="background:var(--gnl);color:var(--gn);font-size:.62rem">RAG</span>':'';
|
||||
// Params panel - available for any model (direct or preset)
|
||||
const pp=S.chatParams;
|
||||
const paramsPanel=hasModel&&S.chatParamsOpen?`<div class="ch-params"><div class="g3">
|
||||
<div><label>Temperature</label><input type="number" value="${pp.temperature}" step="0.1" min="0" max="2" onchange="S.chatParams.temperature=+this.value"></div>
|
||||
<div><label>Max Tokens</label><input type="number" value="${pp.max_tokens}" min="1" max="128000" onchange="S.chatParams.max_tokens=+this.value"></div>
|
||||
<div><label>Top P</label><input type="number" value="${pp.top_p}" step="0.05" min="0" max="1" onchange="S.chatParams.top_p=+this.value"></div>
|
||||
</div><div class="g3">
|
||||
<div><label>Top K</label><input type="number" value="${pp.top_k}" min="-1" max="500" onchange="S.chatParams.top_k=+this.value"></div>
|
||||
<div><label>Freq Penalty</label><input type="number" value="${pp.frequency_penalty}" step="0.1" min="0" max="2" onchange="S.chatParams.frequency_penalty=+this.value"></div>
|
||||
<div><label>Pres Penalty</label><input type="number" value="${pp.presence_penalty}" step="0.1" min="0" max="2" onchange="S.chatParams.presence_penalty=+this.value"></div>
|
||||
</div><div style="margin-top:.5rem;display:flex;align-items:center;gap:.5rem;padding:.3rem 0">
|
||||
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.76rem"><input type="checkbox" ${S.chatUseTools?'checked':''} onchange="S.chatUseTools=this.checked"> 🔌 Usar MCP Tools no Chat</label>
|
||||
${S.mcpSvr.filter(m=>m.is_active&&Array.isArray(m.tools)&&m.tools.length).length?`<span style="font-size:.66rem;color:var(--t4)">(${S.mcpSvr.filter(m=>m.is_active&&Array.isArray(m.tools)&&m.tools.length).reduce((a,m)=>a+m.tools.length,0)} tools disponíveis)</span>`:''}</div></div>`:'';
|
||||
const toolCount=S.mcpSvr.filter(m=>m.is_active&&Array.isArray(m.tools)&&m.tools.length).reduce((a,m)=>a+m.tools.length,0);
|
||||
let sideContent='';
|
||||
if(S.chatPanel==='config')sideContent=rChatConfig(isDirect,toolCount);
|
||||
else if(S.chatPanel==='logs')sideContent=rChatLogs();
|
||||
return`<div class="ch-c">
|
||||
<div class="ch-side${S.chatPanel?' open':''}"><div class="ch-side-inner">${sideContent}</div></div>
|
||||
<div class="ch-main">
|
||||
<div class="ch-t"><label>🧠 Modelo:</label><div class="mdrop"><div class="mdrop-btn" onclick="toggleModelDrop()">${curLabel} <span class="arr">▼</span></div><div class="mdrop-dd" id="mdd"><input type="text" id="mds" placeholder="Buscar modelo..." oninput="filterModels(this.value)"><div class="mdrop-list" id="mdl">${ddItems}</div></div></div>
|
||||
${ociSel}${gearBtn}${ragBadge}<div style="flex:1"></div><button class="btn bd bsm" onclick="clrChat()">Limpar</button></div>
|
||||
${paramsPanel}<div class="ch-m" id="chm">${ms}</div>
|
||||
${S.chatFiles.length?`<div style="display:flex;gap:.4rem;flex-wrap:wrap;padding:.4rem .85rem;background:var(--bg1);border:1px solid var(--bd);border-bottom:none">${S.chatFiles.map((f,i)=>f.type==='image'?`<div style="position:relative;width:48px;height:48px"><img src="${f.preview}" style="width:48px;height:48px;object-fit:cover;border-radius:6px;border:1px solid var(--bd)"><span onclick="rmChatFile(${i})" style="position:absolute;top:-4px;right:-4px;background:var(--err);color:#fff;width:16px;height:16px;border-radius:50%;font-size:.6rem;display:flex;align-items:center;justify-content:center;cursor:pointer">×</span></div>`:`<div style="position:relative;display:flex;align-items:center;gap:.25rem;background:var(--bg2);border:1px solid var(--bd);border-radius:6px;padding:.25rem .5rem;font-size:.68rem"><span>📄 ${f.name}</span><span onclick="rmChatFile(${i})" style="color:var(--err);cursor:pointer;font-weight:700">×</span></div>`).join('')}</div>`:''}
|
||||
${ociSel}${ragBadge}<div style="flex:1"></div>
|
||||
<div class="ch-icon-btn${S.chatPanel==='config'?' active':''}" onclick="togglePanel('config')" title="Configurações">⚙️</div>
|
||||
<div class="ch-icon-btn${S.chatPanel==='logs'?' active':''}" onclick="togglePanel('logs')" title="Chat Logs">📋</div>
|
||||
<div class="ch-icon-btn" onclick="clrChat()" title="Limpar conversa">🗑️</div></div>
|
||||
<div class="ch-m" id="chm">${ms}</div>
|
||||
${S.chatFiles.length?`<div style="display:flex;gap:.4rem;flex-wrap:wrap;padding:.4rem .85rem;background:var(--bg1);border-top:1px solid var(--bd)">${S.chatFiles.map((f,i)=>f.type==='image'?`<div style="position:relative;width:48px;height:48px"><img src="${f.preview}" style="width:48px;height:48px;object-fit:cover;border-radius:6px;border:1px solid var(--bd)"><span onclick="rmChatFile(${i})" style="position:absolute;top:-4px;right:-4px;background:var(--err);color:#fff;width:16px;height:16px;border-radius:50%;font-size:.6rem;display:flex;align-items:center;justify-content:center;cursor:pointer">×</span></div>`:`<div style="position:relative;display:flex;align-items:center;gap:.25rem;background:var(--bg2);border:1px solid var(--bd);border-radius:6px;padding:.25rem .5rem;font-size:.68rem"><span>📄 ${f.name}</span><span onclick="rmChatFile(${i})" style="color:var(--err);cursor:pointer;font-weight:700">×</span></div>`).join('')}</div>`:''}
|
||||
<div class="ch-i"><input type="file" id="chf" multiple accept="image/*,.pdf,.txt,.csv,.json,.log,.xml,.yaml,.yml,.md,.py,.js,.html,.css,.sql" style="display:none" onchange="addChatFiles(this)">
|
||||
<button class="btn bs" onclick="document.getElementById('chf').click()" title="Anexar arquivo" style="font-size:1rem;padding:.3rem .5rem">📎</button>
|
||||
<input type="text" id="chi" placeholder="Digite sua mensagem..." onkeydown="if(event.key==='Enter')sChat()">
|
||||
<button class="btn bp" onclick="sChat()">Enviar →</button></div></div>`}
|
||||
<button class="btn bp" onclick="sChat()">Enviar →</button></div></div></div>`}
|
||||
function rChatConfig(isDirect,toolCount){
|
||||
const pp=S.chatParams;
|
||||
return`<div class="ch-side-title">⚙️ Configurações</div>
|
||||
<div class="ch-side-section"><div class="g2">
|
||||
<div><label>Temperature</label><input type="number" value="${pp.temperature}" step="0.1" min="0" max="2" onchange="S.chatParams.temperature=+this.value"></div>
|
||||
<div><label>Max Tokens</label><input type="number" value="${pp.max_tokens}" min="1" max="128000" onchange="S.chatParams.max_tokens=+this.value"></div>
|
||||
<div><label>Top P</label><input type="number" value="${pp.top_p}" step="0.05" min="0" max="1" onchange="S.chatParams.top_p=+this.value"></div>
|
||||
<div><label>Top K</label><input type="number" value="${pp.top_k}" min="-1" max="500" onchange="S.chatParams.top_k=+this.value"></div>
|
||||
<div><label>Freq Penalty</label><input type="number" value="${pp.frequency_penalty}" step="0.1" min="0" max="2" onchange="S.chatParams.frequency_penalty=+this.value"></div>
|
||||
<div><label>Pres Penalty</label><input type="number" value="${pp.presence_penalty}" step="0.1" min="0" max="2" onchange="S.chatParams.presence_penalty=+this.value"></div>
|
||||
</div></div>
|
||||
<div class="ch-side-section" style="border-top:1px solid var(--bd);padding-top:.65rem">
|
||||
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.76rem"><input type="checkbox" ${S.chatUseTools?'checked':''} onchange="S.chatUseTools=this.checked"> 🔌 MCP Tools</label>
|
||||
${toolCount?`<span style="font-size:.66rem;color:var(--t4);margin-top:.2rem;display:block">${toolCount} tools disponíveis</span>`:''}
|
||||
</div>
|
||||
${isDirect?`<div class="ch-side-section" style="border-top:1px solid var(--bd);padding-top:.65rem">
|
||||
<label>Credencial OCI</label>
|
||||
<select onchange="S.chatOci=this.value;const c=S.ociCfg.find(x=>x.id===this.value);if(c){S.chatRegion=c.region;S.chatCompartment=c.compartment_id||''}" style="margin-top:3px">
|
||||
<option value="">Selecionar...</option>${S.ociCfg.map(c=>`<option value="${c.id}" ${S.chatOci===c.id?'selected':''}>${c.tenancy_name} (${c.region})</option>`).join('')}</select></div>`:''}`}
|
||||
function rChatLogs(){
|
||||
return`<div class="ch-side-title">📋 Chat Logs</div>
|
||||
<div style="display:flex;gap:.4rem;margin-bottom:.5rem">
|
||||
<select id="cl-sev-chat" onchange="loadChatLogs()" style="flex:1;font-size:.7rem;padding:.25rem .4rem;border-radius:6px;border:1px solid var(--bd)">
|
||||
<option value="">Todos</option><option value="error">Erros</option><option value="warning">Avisos</option><option value="info">Info</option></select>
|
||||
<button class="btn bs bsm" onclick="loadChatLogs()" style="font-size:.68rem">↻</button></div>
|
||||
<div id="chat-logs-body"><div style="padding:.5rem;font-size:.74rem;color:var(--t4)">Carregando...</div></div>`}
|
||||
function toggleModelDrop(){const dd=document.getElementById('mdd');dd.classList.toggle('open');if(dd.classList.contains('open')){const inp=document.getElementById('mds');inp.value='';inp.focus();filterModels('')}}
|
||||
function pickModel(v){S.chatModel=v;document.getElementById('mdd').classList.remove('open');
|
||||
if(v&&!v.startsWith('cfg:')&&!S.chatOci&&S.ociCfg.length){S.chatOci=S.ociCfg[0].id;const c=S.ociCfg[0];S.chatRegion=c.region;S.chatCompartment=c.compartment_id||''}
|
||||
@@ -341,9 +452,9 @@ function filterModels(q){const list=document.getElementById('mdl');const items=l
|
||||
else{const show=!q||el.textContent.toLowerCase().includes(ql);el.style.display=show?'':'none';if(show)grpHasVisible=true}});
|
||||
if(lastGrp)lastGrp.style.display=grpHasVisible?'':'none'}
|
||||
document.addEventListener('click',e=>{const dd=document.getElementById('mdd');if(dd&&dd.classList.contains('open')&&!e.target.closest('.mdrop'))dd.classList.remove('open');
|
||||
if(!e.target.closest('.dd')){let changed=false;if(S.rptOciOpen){S.rptOciOpen=false;changed=true}if(S.rptRselOpen){S.rptRselOpen=false;changed=true}if(S.ociFormRegOpen){S.ociFormRegOpen=false;changed=true}if(S.rptRegionsOpen){S.rptRegionsOpen=false;changed=true}if(changed)R()}})
|
||||
if(!e.target.closest('.dd')){let changed=false;if(S.rptOciOpen){S.rptOciOpen=false;changed=true}if(S.rptRselOpen){S.rptRselOpen=false;changed=true}if(S.ociFormRegOpen){S.ociFormRegOpen=false;changed=true}if(S.rptRegionsOpen){S.rptRegionsOpen=false;changed=true}if(S.expRegDdOpen){S.expRegDdOpen=false;changed=true}if(changed)R()}})
|
||||
function chatOciChanged(){S.chatOci=document.getElementById('coci').value;const c=S.ociCfg.find(x=>x.id===S.chatOci);if(c){S.chatRegion=c.region;S.chatCompartment=c.compartment_id||''}}
|
||||
function toggleChatParams(){S.chatParamsOpen=!S.chatParamsOpen;R()}
|
||||
function togglePanel(p){S.chatPanel=S.chatPanel===p?'':p;R();if(S.chatPanel==='logs')loadChatLogs()}
|
||||
function editPrompt(id){S.editingPrompt=id;R()}
|
||||
async function savePrompt(){const name=document.getElementById('spname').value;const content=document.getElementById('gsysp').value;
|
||||
if(!name||!content)return sm('spm','Preencha nome e conteúdo do prompt.','e');
|
||||
@@ -419,27 +530,549 @@ async function pollChatResult(mid){
|
||||
S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:'⏰ Timeout: a resposta está demorando muito. Tente novamente.',t:tstamp()});R()}
|
||||
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()}
|
||||
async function loadChatLogs(){
|
||||
const sev=document.getElementById('cl-sev-chat')?.value||'';
|
||||
const qs='limit=50'+(sev?'&severity='+sev:'')+(S.sid?'&session_id='+S.sid:'');
|
||||
const el=document.getElementById('chat-logs-body');if(!el)return;
|
||||
try{const logs=await $api('/chat-logs?'+qs);
|
||||
if(!logs.length){el.innerHTML='<div class="emp" style="padding:.5rem"><p>Nenhum log registrado.</p></div>';return}
|
||||
el.innerHTML='<table style="font-size:.74rem"><thead><tr><th>Data</th><th>Fonte</th><th>Ação</th><th>Status</th><th>Mensagem</th></tr></thead><tbody>'+
|
||||
logs.map(l=>{const sc=l.severity==='error'?'err':l.severity==='warning'?'w':'ok';
|
||||
return'<tr><td style="font-size:.62rem;color:var(--t4);white-space:nowrap">'+l.created_at+'</td>'+
|
||||
'<td><span class="tag">'+l.source+'</span></td>'+
|
||||
'<td style="font-size:.72rem">'+l.action+'</td>'+
|
||||
'<td><span style="color:var(--'+sc+');font-weight:600;font-size:.7rem">'+l.severity+'</span></td>'+
|
||||
'<td style="font-size:.72rem;max-width:400px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="'+(l.message||'').replace(/"/g,'"')+'">'+(l.message?.slice(0,150)||'—')+'</td></tr>'}).join('')+
|
||||
'</tbody></table>'}
|
||||
catch(e){el.innerHTML='<div class="al al-e">'+e.message+'</div>'}}
|
||||
|
||||
/* ── Terraform Agent ── */
|
||||
const TF_ICON=`<svg viewBox="0 0 16 16" width="16" height="16"><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 escHtml(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
||||
|
||||
function fmTf(t){
|
||||
// Extract ALL hcl blocks and build files list
|
||||
const codeBlocks=[];
|
||||
let r=t.replace(/```(?:hcl|terraform)\s*\n([\s\S]*?)```/g,(m,code)=>{
|
||||
const trimmed=code.trim();
|
||||
codeBlocks.push(trimmed);
|
||||
S.tfCode=trimmed;
|
||||
return '<div class="tf-code-block"><div class="tf-code-header"><span>'+TF_ICON+' main.tf</span>'+
|
||||
'<button class="btn" onclick="copyTfCode()">Copy</button></div>'+
|
||||
'<pre class="tf-pre"><code>'+escHtml(trimmed)+'</code></pre></div>';
|
||||
});
|
||||
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>';
|
||||
});
|
||||
// Update files list
|
||||
if(codeBlocks.length){
|
||||
S.tfFiles=[{name:'main.tf',content:codeBlocks.join('\n\n'),size:codeBlocks.join('\n\n').length,type:'hcl'}];
|
||||
// Detect if the code has variables -> also create variables.tf reference
|
||||
}
|
||||
r=r.replace(/\*\*(.*?)\*\*/g,'<strong>$1</strong>').replace(/`(.*?)`/g,'<code>$1</code>').replace(/\n/g,'<br>');
|
||||
return r;
|
||||
}
|
||||
|
||||
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></div>`).join('');
|
||||
|
||||
// Model dropdown
|
||||
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='';
|
||||
if(S.genaiCfg.length){ddItems+='<div class="grp">Configs Salvas</div>';S.genaiCfg.forEach(g=>{const mi=S.models[g.model_id];ddItems+=`<div class="itm${S.tfModel==='cfg:'+g.id?' sel':''}" onclick="tfPickModel('cfg:${g.id}')">${g.name||mi?.name||g.model_id}</div>`})}
|
||||
provOrder.filter(p=>provs[p]).forEach(p=>{ddItems+=`<div class="grp">${p[0].toUpperCase()+p.slice(1)}</div>`;provs[p].forEach(m=>{ddItems+=`<div class="itm${S.tfModel===m.id?' sel':''}" onclick="tfPickModel('${m.id}')">${m.name}</div>`})});
|
||||
let curLabel='Selecione modelo...';
|
||||
if(S.tfModel.startsWith('cfg:')){const g=S.genaiCfg.find(x=>x.id===S.tfModel.slice(4));curLabel=g?(g.name||g.model_id):'Config...'}
|
||||
else if(S.tfModel){const mi=S.models[S.tfModel];curLabel=mi?mi.name:S.tfModel}
|
||||
|
||||
const isDirect=S.tfModel&&!S.tfModel.startsWith('cfg:');
|
||||
const ociSel=isDirect?`<select id="tfoci" onchange="S.tfOci=this.value;const c=S.ociCfg.find(x=>x.id===this.value);if(c){S.tfRegion=c.region;S.tfCompartment=c.compartment_id||''}tfLoadComps()" style="max-width:180px"><option value="">OCI Config...</option>${S.ociCfg.map(c=>`<option value="${c.id}"${S.tfOci===c.id?' selected':''}>${c.tenancy_name} (${c.region})</option>`).join('')}</select>`:'';
|
||||
const compSel=(S.tfComps.length||S.tfCompLoading)?`<select onchange="S.tfCompartment=this.value;tfLoadResources()" style="max-width:220px">${S.tfCompLoading?'<option>Carregando...</option>':'<option value="">Compartment...</option>'+tfFlatComps(S.tfComps,0).map(c=>`<option value="${c.id}"${S.tfCompartment===c.id?' selected':''}>${c.name}</option>`).join('')}</select>`:'';
|
||||
|
||||
// Status badge
|
||||
const stBadge=_tfBadge(S.tfStatus);
|
||||
|
||||
return `<div class="tf-wrap">
|
||||
<div class="tf-chat">
|
||||
<div class="tf-toolbar">
|
||||
${TF_ICON}
|
||||
<div class="mdrop"><div class="mdrop-btn" onclick="S.tfMdOpen=!S.tfMdOpen;R()">${curLabel} <span class="arr">▼</span></div>
|
||||
${S.tfMdOpen?`<div class="mdrop-dd open"><input type="text" id="tfmds" placeholder="Buscar..." oninput="tfFilterModels(this.value)"><div class="mdrop-list" id="tfmdl">${ddItems}</div></div>`:''}
|
||||
</div>
|
||||
${ociSel}${compSel}
|
||||
${stBadge}
|
||||
<div style="flex:1"></div>
|
||||
${rTfActions()}
|
||||
<div class="tf-file-btn" onclick="tfClear()" title="Nova conversa" style="width:24px;height:24px"><svg viewBox="0 0 16 16" width="12" height="12" fill="var(--t3)"><path d="M2 3h12v1H2zm1.5-1h9l-.5-1h-8zM3 4v9.5a1.5 1.5 0 001.5 1.5h7a1.5 1.5 0 001.5-1.5V4z"/></svg></div>
|
||||
</div>
|
||||
<div class="tf-msgs" id="tfchm">${ms}</div>
|
||||
<div class="tf-input">
|
||||
<input type="text" id="tfi" placeholder="Descreva a infraestrutura desejada..." onkeydown="if(event.key==='Enter')tfSend()">
|
||||
<button class="btn bp" onclick="tfSend()" style="background:#7b42bc;border-color:#7b42bc">Gerar</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tf-resize" onmousedown="tfStartResize(event)"></div>
|
||||
<div class="tf-bottom" id="tfBottom">
|
||||
<div class="tf-bottom-tabs">
|
||||
<div class="tf-btab${S.tfBtab==='files'?' active':''}" onclick="S.tfBtab='files';R()">
|
||||
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M1 2.5A1.5 1.5 0 012.5 1h3.379a1.5 1.5 0 011.06.44l1.122 1.12A1.5 1.5 0 009.121 3H13.5A1.5 1.5 0 0115 4.5v8a1.5 1.5 0 01-1.5 1.5h-11A1.5 1.5 0 011 12.5z"/></svg>
|
||||
Files${S.tfFiles.length?' ('+S.tfFiles.length+')':''}
|
||||
</div>
|
||||
<div class="tf-btab${S.tfBtab==='plan'?' active':''}" onclick="S.tfBtab='plan';R()">
|
||||
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M2 2h12v2H2zm0 4h8v2H2zm0 4h10v2H2z"/></svg>
|
||||
Plan${S.tfPlan.length?' ('+S.tfPlan.length+')':''}
|
||||
</div>
|
||||
<div class="tf-btab${S.tfBtab==='resources'?' active':''}" onclick="S.tfBtab='resources';if(!S.tfResources&&S.tfCompartment)tfLoadResources();R()">
|
||||
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M8 1L1 5l7 4 7-4zm0 6L1 11l7 4 7-4z"/></svg>
|
||||
Resources${S.tfResources?(' ('+Object.values(S.tfResources).reduce((a,b)=>a+(Array.isArray(b)?b.length:0),0)+')'):''}
|
||||
</div>
|
||||
<div class="tf-btab${S.tfBtab==='output'?' active':''}" onclick="S.tfBtab='output';R()">
|
||||
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M2 1h12a1 1 0 011 1v12a1 1 0 01-1 1H2a1 1 0 01-1-1V2a1 1 0 011-1zm1 3v9h10V4zm2 1l3 2.5L5 10z"/></svg>
|
||||
Output
|
||||
</div>
|
||||
<div class="tf-bottom-actions">
|
||||
${S.tfFiles.length?'<button class="btn bs bsm" onclick="tfDlAll()" style="font-size:.66rem;padding:2px 8px">Download All</button>':''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tf-bottom-content">${rTfBottomContent()}</div>
|
||||
</div>
|
||||
${S.tfConfirmDest?rTfConfirmDestroy():''}
|
||||
</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']};
|
||||
const[cls,label]=m[st]||['draft',st];
|
||||
return`<span class="tf-badge tf-badge-${cls}">${label}</span>`;
|
||||
}
|
||||
|
||||
function rTfActions(){
|
||||
if(S.tfRunning)return'<button class="btn bsm" style="background:var(--rd);color:#fff;border-color:var(--rd);font-size:.68rem;padding:2px 10px" onclick="tfCancel()">Cancel</button>';
|
||||
const st=S.tfStatus;let btns='';
|
||||
if(S.tfCode&&(st==='draft'||st==='failed'))btns+=`<button class="btn bsm" style="background:#7b42bc;color:#fff;border-color:#7b42bc;font-size:.68rem;padding:2px 10px" onclick="tfSaveAndPlan()">${TF_ICON} Plan</button>`;
|
||||
if(st==='planned'||st==='failed')btns+=`<button class="btn bsm" style="background:#7b42bc;color:#fff;border-color:#7b42bc;font-size:.68rem;padding:2px 10px" onclick="tfSaveAndPlan()">${TF_ICON} Re-plan</button>`;
|
||||
if(st==='planned')btns+='<button class="btn bsm" style="background:var(--gn);color:#fff;border-color:var(--gn);font-size:.68rem;padding:2px 10px" onclick="tfApply()">Apply</button>';
|
||||
if(st==='applied')btns+=`<button class="btn bsm" style="background:#7b42bc;color:#fff;border-color:#7b42bc;font-size:.68rem;padding:2px 10px" onclick="tfSaveAndPlan()">${TF_ICON} Re-plan</button>`;
|
||||
if(st==='applied'||st==='planned')btns+='<button class="btn bsm" style="background:var(--rd);color:#fff;border-color:var(--rd);font-size:.68rem;padding:2px 10px" onclick="S.tfConfirmDest=true;R()">Destroy</button>';
|
||||
return btns;
|
||||
}
|
||||
|
||||
function rTfBottomContent(){
|
||||
if(S.tfBtab==='files')return rTfFiles();
|
||||
if(S.tfBtab==='plan')return rTfPlanPanel();
|
||||
if(S.tfBtab==='resources')return rTfResourcesPanel();
|
||||
if(S.tfBtab==='output')return rTfOutputPanel();
|
||||
return'';
|
||||
}
|
||||
|
||||
function rTfFiles(){
|
||||
if(!S.tfFiles.length)return`<div class="tf-empty">${TF_ICON}<p>Nenhum arquivo gerado ainda</p><p style="font-size:.66rem;opacity:.5">Descreva a infraestrutura desejada no chat acima</p></div>`;
|
||||
// Auto-add provider.tf and terraform.tfvars as virtual files
|
||||
const files=[...S.tfFiles];
|
||||
if(S.tfWs||S.tfOci){
|
||||
const oc=S.ociCfg.find(x=>x.id===S.tfOci);
|
||||
if(oc){
|
||||
const comp=S.tfCompartment||oc.compartment_id||'';
|
||||
if(!files.find(f=>f.name==='terraform.tfvars'))files.push({name:'terraform.tfvars',content:`compartment_id = "${comp}"\nregion = "${oc.region}"`,size:0,type:'tfvars',auto:true});
|
||||
}
|
||||
}
|
||||
return files.map((f,i)=>{
|
||||
const sz=f.content?f.content.length:f.size;
|
||||
const sizeStr=sz>1024?(sz/1024).toFixed(1)+' KB':sz+' B';
|
||||
const iconFill=f.auto?'#888':'#7b42bc';
|
||||
return`<div class="tf-file">
|
||||
<div class="tf-file-icon"><svg viewBox="0 0 16 16" fill="${iconFill}"><path d="M4 1a1 1 0 00-1 1v12a1 1 0 001 1h8a1 1 0 001-1V4.5L9.5 1H4zm5 0v3.5h3.5"/></svg></div>
|
||||
<div class="tf-file-info"><div class="tf-file-name">${f.name}</div><div class="tf-file-meta">${sizeStr}${f.auto?' · auto-generated':''}</div></div>
|
||||
<div class="tf-file-actions">
|
||||
<div class="tf-file-btn" onclick="tfCopyFile(${i})" title="Copiar"><svg viewBox="0 0 16 16" width="12" height="12" fill="var(--t3)"><path d="M4 2a2 2 0 012-2h4.586L14 3.414V10a2 2 0 01-2 2H6a2 2 0 01-2-2zm6-1.414L13.414 4H10zM2 6a2 2 0 00-2 2v6a2 2 0 002 2h4a2 2 0 002-2v-1H6a3 3 0 01-3-3V6z"/></svg></div>
|
||||
<div class="tf-file-btn" onclick="tfDlFile(${i})" title="Download"><svg viewBox="0 0 16 16" width="12" height="12" fill="var(--t3)"><path d="M8 1v9m0 0L5 7m3 3l3-3M2 12v1.5A1.5 1.5 0 003.5 15h9a1.5 1.5 0 001.5-1.5V12" stroke="var(--t3)" stroke-width="1.5" fill="none" stroke-linecap="round"/></svg></div>
|
||||
</div>
|
||||
</div>`}).join('');
|
||||
}
|
||||
|
||||
function rTfPlanPanel(){
|
||||
if(!S.tfPlan.length)return`<div class="tf-empty">${TF_ICON}<p>Nenhum recurso no plano</p></div>`;
|
||||
return S.tfPlan.map(r=>`<div style="display:flex;align-items:center;gap:.5rem;padding:.4rem .5rem;border-radius:8px;background:var(--gnl);margin-bottom:.3rem">
|
||||
<span style="color:var(--gn);font-weight:700;font-size:.82rem;font-family:var(--fm)">+</span>
|
||||
<div><div style="font-size:.74rem;font-weight:600;font-family:var(--fm);color:var(--t1)">${escHtml(r.type)}</div>
|
||||
<div style="font-size:.64rem;color:var(--t3)">${escHtml(r.desc)}</div></div></div>`).join('');
|
||||
}
|
||||
|
||||
function rTfOutputPanel(){
|
||||
let out='';
|
||||
if(S.tfPlanOut)out+=`<div style="font-size:.7rem;font-weight:600;color:#7b42bc;margin-bottom:.3rem">${TF_ICON} Plan Output</div><div class="tf-output">${escHtml(S.tfPlanOut)}</div>`;
|
||||
if(S.tfApplyOut)out+=`<div style="font-size:.7rem;font-weight:600;color:var(--gn);margin-top:.5rem;margin-bottom:.3rem">Apply Output</div><div class="tf-output">${escHtml(S.tfApplyOut)}</div>`;
|
||||
if(S.tfDestroyOut)out+=`<div style="font-size:.7rem;font-weight:600;color:var(--rd);margin-top:.5rem;margin-bottom:.3rem">Destroy Output</div><div class="tf-output">${escHtml(S.tfDestroyOut)}</div>`;
|
||||
if(!out)return`<div class="tf-empty"><svg viewBox="0 0 16 16" width="36" height="36" fill="#7b42bc" opacity=".2"><path d="M2 1h12a1 1 0 011 1v12a1 1 0 01-1 1H2a1 1 0 01-1-1V2a1 1 0 011-1zm1 3v9h10V4zm2 1l3 2.5L5 10z"/></svg><p>Nenhuma execução ainda</p></div>`;
|
||||
return out;
|
||||
}
|
||||
|
||||
function rTfConfirmDestroy(){
|
||||
return`<div class="tf-modal-overlay" onclick="S.tfConfirmDest=false;R()"><div class="tf-modal" onclick="event.stopPropagation()">
|
||||
<h3 style="display:flex;align-items:center;gap:.4rem"><svg viewBox="0 0 16 16" width="18" height="18" fill="var(--rd)"><path d="M8 1a7 7 0 100 14A7 7 0 008 1zm0 2.5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 018 3.5zM8 10a1 1 0 100 2 1 1 0 000-2z"/></svg> Confirmar Destroy</h3>
|
||||
<p style="font-size:.8rem;margin:.5rem 0">Isso irá <strong>destruir todos os recursos</strong> provisionados. Esta ação é irreversível.</p>
|
||||
<p style="font-size:.74rem;color:var(--t3);margin-bottom:.8rem">Digite <strong>DESTROY</strong> para confirmar:</p>
|
||||
<input type="text" id="tfDestInput" placeholder="DESTROY" style="margin-bottom:.6rem">
|
||||
<div style="display:flex;gap:.5rem;justify-content:flex-end">
|
||||
<button class="btn bs" onclick="S.tfConfirmDest=false;R()">Cancelar</button>
|
||||
<button class="btn bd" onclick="tfConfirmDestroy()">Destruir</button>
|
||||
</div>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
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 onUp(){bar.classList.remove('dragging');document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp)}
|
||||
document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);
|
||||
}
|
||||
|
||||
function tfCopyFile(i){
|
||||
const files=[...S.tfFiles];
|
||||
const oc=S.ociCfg.find(x=>x.id===S.tfOci);
|
||||
if(oc){const comp=S.tfCompartment||oc.compartment_id||'';if(!files.find(f=>f.name==='terraform.tfvars'))files.push({name:'terraform.tfvars',content:`compartment_id = "${comp}"\nregion = "${oc.region}"`,auto:true})}
|
||||
if(files[i])navigator.clipboard.writeText(files[i].content).then(()=>alert('Copiado!'));
|
||||
}
|
||||
function tfDlFile(i){
|
||||
const files=[...S.tfFiles];
|
||||
const oc=S.ociCfg.find(x=>x.id===S.tfOci);
|
||||
if(oc){const comp=S.tfCompartment||oc.compartment_id||'';if(!files.find(f=>f.name==='terraform.tfvars'))files.push({name:'terraform.tfvars',content:`compartment_id = "${comp}"\nregion = "${oc.region}"`,auto:true})}
|
||||
const f=files[i];if(!f)return;
|
||||
const b=new Blob([f.content],{type:'text/plain'});const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download=f.name;a.click();URL.revokeObjectURL(a.href);
|
||||
}
|
||||
function tfDlAll(){
|
||||
// Download each file
|
||||
const files=[...S.tfFiles];
|
||||
const oc=S.ociCfg.find(x=>x.id===S.tfOci);
|
||||
if(oc){const comp=S.tfCompartment||oc.compartment_id||'';if(!files.find(f=>f.name==='terraform.tfvars'))files.push({name:'terraform.tfvars',content:`compartment_id = "${comp}"\nregion = "${oc.region}"`,auto:true})}
|
||||
files.forEach((f,i)=>{setTimeout(()=>{const b=new Blob([f.content],{type:'text/plain'});const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download=f.name;a.click();URL.revokeObjectURL(a.href)},i*200)});
|
||||
}
|
||||
|
||||
function tfPickModel(v){S.tfModel=v;S.tfMdOpen=false;if(v&&!v.startsWith('cfg:')&&!S.tfOci&&S.ociCfg.length){S.tfOci=S.ociCfg[0].id;S.tfRegion=S.ociCfg[0].region;S.tfCompartment=S.ociCfg[0].compartment_id||'';tfLoadComps()}else if(v&&v.startsWith('cfg:')){const g=S.genaiCfg.find(x=>x.id===v.slice(4));if(g&&g.oci_config_id&&!S.tfOci){S.tfOci=g.oci_config_id;const c=S.ociCfg.find(x=>x.id===g.oci_config_id);if(c){S.tfRegion=c.region;S.tfCompartment=c.compartment_id||''}tfLoadComps()}}R()}
|
||||
async function tfLoadComps(){
|
||||
const oid=S.tfOci||(S.tfModel.startsWith('cfg:')?((S.genaiCfg.find(x=>x.id===S.tfModel.slice(4))||{}).oci_config_id||''):'');
|
||||
if(!oid){S.tfComps=[];R();return}
|
||||
S.tfCompLoading=true;R();
|
||||
try{const d=await $api('/oci/explore/'+oid+'/compartment-tree');S.tfComps=d&&!d.error?d:[];
|
||||
if(S.tfComps.length&&!S.tfCompartment){S.tfCompartment=S.tfComps[0].id;tfLoadResources()}
|
||||
}catch(e){S.tfComps=[]}
|
||||
S.tfCompLoading=false;R();
|
||||
}
|
||||
function tfFlatComps(comps,depth){
|
||||
let r=[];for(const c of comps){r.push({id:c.id,name:' '.repeat(depth)+c.name,depth});if(c.children&&c.children.length)r=r.concat(tfFlatComps(c.children,depth+1))}return r;
|
||||
}
|
||||
async function tfLoadResources(){
|
||||
const oid=S.tfOci||(S.tfModel.startsWith('cfg:')?((S.genaiCfg.find(x=>x.id===S.tfModel.slice(4))||{}).oci_config_id||''):'');
|
||||
const comp=S.tfCompartment;
|
||||
if(!oid||!comp){S.tfResources=null;R();return}
|
||||
S.tfResLoading=true;R();
|
||||
try{const d=await $api('/terraform/resources?oci_config_id='+encodeURIComponent(oid)+'&compartment_id='+encodeURIComponent(comp)+(S.tfRegion?'®ion='+encodeURIComponent(S.tfRegion):''));
|
||||
S.tfResources=d&&!d.error?d:null;
|
||||
}catch(e){S.tfResources=null}
|
||||
S.tfResLoading=false;R();
|
||||
}
|
||||
function rTfResourcesPanel(){
|
||||
if(S.tfResLoading)return`<div class="tf-empty"><div class="spinner"></div><p>Carregando recursos...</p></div>`;
|
||||
if(!S.tfCompartment)return`<div class="tf-empty"><svg viewBox="0 0 16 16" width="36" height="36" fill="#7b42bc" opacity=".2"><path d="M8 1L1 5l7 4 7-4zm0 6L1 11l7 4 7-4z"/></svg><p>Selecione um compartment para ver os recursos existentes</p></div>`;
|
||||
if(!S.tfResources)return`<div class="tf-empty"><svg viewBox="0 0 16 16" width="36" height="36" fill="#7b42bc" opacity=".2"><path d="M8 1L1 5l7 4 7-4zm0 6L1 11l7 4 7-4z"/></svg><p>Nenhum recurso carregado</p><button class="btn bs bsm" onclick="tfLoadResources()" style="margin-top:.5rem">Carregar Recursos</button></div>`;
|
||||
const res=S.tfResources;
|
||||
const cats=[
|
||||
{key:'vcns',label:'VCNs',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M1 3h14v2H1zm2 4h10v2H3zm3 4h4v2H6z"/></svg>',fmt:r=>`${r.display_name} <span style="opacity:.6">(${(r.cidr_blocks||[]).join(', ')})</span>`},
|
||||
{key:'subnets',label:'Subnets',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M2 2h5v5H2zm7 0h5v5H9zm-7 7h5v5H2zm7 0h5v5H9z"/></svg>',fmt:r=>`${r.display_name} <span style="opacity:.6">(${r.cidr_block}, ${r.public?'pública':'privada'})</span>`},
|
||||
{key:'instances',label:'Compute',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M2 2h12v12H2z"/></svg>',fmt:r=>`${r.display_name} <span style="opacity:.6">(${r.shape})</span>`},
|
||||
{key:'internet_gateways',label:'Internet GWs',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><circle cx="8" cy="8" r="6" fill="none" stroke="#7b42bc" stroke-width="1.5"/><path d="M1 8h14M8 2c-2 2-2 10 0 12m0-12c2 2 2 10 0 12"/></svg>',fmt:r=>r.display_name},
|
||||
{key:'nat_gateways',label:'NAT GWs',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M3 3l5 5-5 5"/><path d="M8 8h6" stroke="#7b42bc" stroke-width="1.5"/></svg>',fmt:r=>r.display_name},
|
||||
{key:'route_tables',label:'Route Tables',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M2 3h12v2H2zm0 4h8v2H2zm0 4h10v2H2z"/></svg>',fmt:r=>r.display_name},
|
||||
{key:'security_lists',label:'Security Lists',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M8 1l6 3v4c0 3.5-2.5 6.5-6 7.5C4.5 14.5 2 11.5 2 8V4z"/></svg>',fmt:r=>r.display_name},
|
||||
{key:'autonomous_databases',label:'Databases',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><ellipse cx="8" cy="4" rx="6" ry="2.5"/><path d="M2 4v8c0 1.4 2.7 2.5 6 2.5s6-1.1 6-2.5V4"/></svg>',fmt:r=>`${r.display_name} <span style="opacity:.6">(${r.db_name}${r.is_free_tier?' Free':''})</span>`},
|
||||
{key:'block_volumes',label:'Block Volumes',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M3 2h10v12H3z"/></svg>',fmt:r=>`${r.display_name} <span style="opacity:.6">(${r.size_in_gbs||'?'} GB)</span>`},
|
||||
{key:'load_balancers',label:'Load Balancers',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M8 2v12M2 8h12M4 4l8 8M12 4l-8 8"/></svg>',fmt:r=>`${r.display_name} <span style="opacity:.6">(${r.shape_name||'flexible'})</span>`}
|
||||
];
|
||||
let total=0;
|
||||
let html='<div style="padding:.5rem">';
|
||||
html+='<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.5rem"><span style="font-size:.72rem;font-weight:600;color:var(--t2)">Recursos existentes no compartment</span><button class="btn bs bsm" onclick="tfLoadResources()" style="font-size:.6rem;padding:1px 6px">Atualizar</button></div>';
|
||||
for(const cat of cats){
|
||||
const items=res[cat.key]||[];
|
||||
if(!items.length)continue;
|
||||
total+=items.length;
|
||||
html+=`<div style="margin-bottom:.6rem"><div style="display:flex;align-items:center;gap:.3rem;font-size:.7rem;font-weight:600;color:#7b42bc;margin-bottom:.2rem">${cat.icon} ${cat.label} (${items.length})</div>`;
|
||||
for(const r of items.slice(0,10)){
|
||||
html+=`<div style="font-size:.68rem;padding:.15rem .4rem .15rem 1.2rem;color:var(--t2);border-left:2px solid #7b42bc22">${cat.fmt(r)}</div>`;
|
||||
}
|
||||
if(items.length>10)html+=`<div style="font-size:.62rem;padding:.1rem .4rem .1rem 1.2rem;color:var(--t3)">... e mais ${items.length-10}</div>`;
|
||||
html+='</div>';
|
||||
}
|
||||
if(!total)html+='<div style="font-size:.72rem;color:var(--t3);text-align:center;padding:1rem">Nenhum recurso encontrado neste compartment</div>';
|
||||
else html+=`<div style="font-size:.66rem;color:var(--t3);border-top:1px solid var(--bd);padding-top:.3rem;margin-top:.3rem">Total: <strong>${total}</strong> recursos. O agente irá reutilizá-los automaticamente quando possível.</div>`;
|
||||
html+='</div>';
|
||||
return html;
|
||||
}
|
||||
function tfFilterModels(q){const list=document.getElementById('tfmdl');if(!list)return;const items=list.querySelectorAll('.itm,.grp');const ql=q.toLowerCase();let lastGrp=null,vis=false;items.forEach(el=>{if(el.classList.contains('grp')){if(lastGrp)lastGrp.style.display=vis?'':'none';lastGrp=el;vis=false}else{const show=!q||el.textContent.toLowerCase().includes(ql);el.style.display=show?'':'none';if(show)vis=true}});if(lastGrp)lastGrp.style.display=vis?'':'none'}
|
||||
function tfTogglePanel(p){S.tfPanel=S.tfPanel===p?'':p;R()}
|
||||
async function tfClear(){if(S.tfSid)try{await $api('/chat/'+S.tfSid,{method:'DELETE'})}catch(e){}S.tfMsgs=[];S.tfSid=null;S.tfPlan=[];S.tfCode='';S.tfWs=null;S.tfPlanOut='';S.tfApplyOut='';S.tfDestroyOut='';S.tfStatus='draft';S.tfRunning=false;S.tfConfirmDest=false;S.tfComps=[];S.tfCompartment='';S.tfResources=null;S.tfResLoading=false;R()}
|
||||
function tfScroll(){setTimeout(()=>{const e=document.getElementById('tfchm');if(e)e.scrollTop=e.scrollHeight},50)}
|
||||
function copyTfCode(){navigator.clipboard.writeText(S.tfCode).then(()=>alert('Código copiado!'))}
|
||||
function dlTfCode(){if(!S.tfCode)return;const b=new Blob([S.tfCode],{type:'text/plain'});const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download='main.tf';a.click();URL.revokeObjectURL(a.href)}
|
||||
|
||||
async function tfSend(){
|
||||
const el=document.getElementById('tfi');const m=el.value.trim();if(!m)return;
|
||||
if(!S.tfModel){S.tfMsgs.push({r:'assistant',c:'⚠️ Selecione um modelo antes de enviar.'});R();return}
|
||||
el.value='';S.tfMsgs.push({r:'user',c:m});R();tfScroll();
|
||||
S.tfMsgs.push({r:'assistant',c:'⏳ _Gerando Terraform..._',thinking:true});R();tfScroll();
|
||||
try{
|
||||
const body={message:m,session_id:S.tfSid,use_tools:false};
|
||||
if(S.tfModel.startsWith('cfg:'))body.genai_config_id=S.tfModel.slice(4);
|
||||
else{
|
||||
if(!S.tfOci){S.tfMsgs.pop();S.tfMsgs.push({r:'assistant',c:'⚠️ Selecione uma credencial OCI.'});R();return}
|
||||
body.model_id=S.tfModel;body.oci_config_id=S.tfOci;body.genai_region=S.tfRegion;body.compartment_id=S.tfCompartment;
|
||||
}
|
||||
const d=await $api('/terraform/chat',{method:'POST',body});
|
||||
S.tfSid=d.session_id;
|
||||
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'});R();tfScroll()}
|
||||
}catch(e){S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:'❌ Erro: '+e.message});R()}
|
||||
}
|
||||
|
||||
async function tfPollResult(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.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:r.content});R();tfScroll();
|
||||
if(r.content&&r.content.includes('```plan')){S.tfPanel='plan';R()}return}
|
||||
if(r.status==='failed'){S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:'❌ '+r.content});R();tfScroll();return}
|
||||
}catch(e){}
|
||||
}
|
||||
S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:'⏰ Timeout.'});R()
|
||||
}
|
||||
|
||||
async function tfSaveAndPlan(){
|
||||
if(!S.tfCode){alert('Gere código Terraform primeiro.');return}
|
||||
// Resolve OCI config
|
||||
let ociId=S.tfOci;
|
||||
if(!ociId&&S.tfModel.startsWith('cfg:')){const g=S.genaiCfg.find(x=>x.id===S.tfModel.slice(4));if(g)ociId=g.oci_config_id}
|
||||
if(!ociId&&S.ociCfg.length)ociId=S.ociCfg[0].id;
|
||||
if(!ociId){alert('Selecione uma credencial OCI.');return}
|
||||
try{
|
||||
// Create or update workspace
|
||||
if(!S.tfWs){
|
||||
const d=await $api('/terraform/workspaces',{method:'POST',body:{session_id:S.tfSid||'none',oci_config_id:ociId,compartment_id:S.tfCompartment||null,name:'workspace',tf_code:S.tfCode}});
|
||||
S.tfWs=d.id;
|
||||
}else{
|
||||
await $api('/terraform/workspaces/'+S.tfWs+'/code',{method:'PUT',body:{tf_code:S.tfCode}});
|
||||
}
|
||||
// Run plan
|
||||
await $api('/terraform/workspaces/'+S.tfWs+'/plan',{method:'POST'});
|
||||
S.tfStatus='planning';S.tfRunning=true;S.tfPlanOut='';S.tfPanel='output';R();
|
||||
await tfPollExec();
|
||||
}catch(e){alert('Erro: '+e.message)}
|
||||
}
|
||||
|
||||
async function tfApply(){
|
||||
if(!S.tfWs){alert('Execute Plan primeiro.');return}
|
||||
try{
|
||||
await $api('/terraform/workspaces/'+S.tfWs+'/apply',{method:'POST'});
|
||||
S.tfStatus='applying';S.tfRunning=true;S.tfApplyOut='';S.tfPanel='output';R();
|
||||
await tfPollExec();
|
||||
}catch(e){alert('Erro: '+e.message)}
|
||||
}
|
||||
|
||||
async function tfConfirmDestroy(){
|
||||
const inp=document.getElementById('tfDestInput');
|
||||
if(!inp||inp.value!=='DESTROY'){alert('Digite DESTROY para confirmar.');return}
|
||||
S.tfConfirmDest=false;R();
|
||||
if(!S.tfWs)return;
|
||||
try{
|
||||
await $api('/terraform/workspaces/'+S.tfWs+'/destroy',{method:'POST',body:{confirm:'DESTROY'}});
|
||||
S.tfStatus='destroying';S.tfRunning=true;S.tfDestroyOut='';S.tfPanel='output';R();
|
||||
await tfPollExec();
|
||||
}catch(e){alert('Erro: '+e.message)}
|
||||
}
|
||||
|
||||
async function tfCancel(){
|
||||
if(!S.tfWs)return;
|
||||
try{await $api('/terraform/workspaces/'+S.tfWs+'/cancel',{method:'POST'})}catch(e){}
|
||||
S.tfRunning=false;S.tfStatus='failed';R();
|
||||
}
|
||||
|
||||
async function tfPollExec(){
|
||||
for(let i=0;i<600;i++){
|
||||
await new Promise(r=>setTimeout(r,2000));
|
||||
try{
|
||||
const r=await $api('/terraform/workspaces/'+S.tfWs+'/status');
|
||||
S.tfPlanOut=r.plan_output||S.tfPlanOut;
|
||||
S.tfApplyOut=r.apply_output||S.tfApplyOut;
|
||||
S.tfDestroyOut=r.destroy_output||S.tfDestroyOut;
|
||||
S.tfStatus=r.status;
|
||||
if(!['planning','applying','destroying'].includes(r.status)){S.tfRunning=false;R();return}
|
||||
R();
|
||||
// Auto-scroll output
|
||||
const outs=document.querySelectorAll('.tf-output');outs.forEach(o=>o.scrollTop=o.scrollHeight);
|
||||
}catch(e){}
|
||||
}
|
||||
S.tfRunning=false;R();
|
||||
}
|
||||
|
||||
|
||||
/* ── OCI Explorer ── */
|
||||
const EXP_GROUPS=[
|
||||
{name:'Compute',tabs:[
|
||||
{key:'instances',label:'Instances'},{key:'boot_volumes',label:'Boot Vol'},{key:'instance_pools',label:'Pools'}]},
|
||||
{name:'Rede',tabs:[
|
||||
{key:'vcns',label:'VCN'},{key:'subnets',label:'Subnets'},{key:'security_lists',label:'Sec Lists'},
|
||||
{key:'nsgs',label:'NSGs'},{key:'route_tables',label:'Routes'},{key:'nat_gateways',label:'NAT GW'},
|
||||
{key:'internet_gateways',label:'IGW'},{key:'service_gateways',label:'SGW'},{key:'public_ips',label:'IPs Públicos'},
|
||||
{key:'load_balancers',label:'Load Balancer'}]},
|
||||
{name:'Storage',tabs:[
|
||||
{key:'buckets',label:'Buckets'},{key:'block_volumes',label:'Block Vol'},
|
||||
{key:'file_systems',label:'File Storage'},{key:'mount_targets',label:'Mount Targets'}]},
|
||||
{name:'Database',tabs:[
|
||||
{key:'databases',label:'ADB'},{key:'db_systems',label:'DB Systems'},{key:'mysql_db_systems',label:'MySQL'}]},
|
||||
{name:'Containers',tabs:[
|
||||
{key:'container_instances',label:'Containers'},{key:'oke_clusters',label:'OKE'}]},
|
||||
{name:'Serverless',tabs:[
|
||||
{key:'functions',label:'Functions'},{key:'api_gateways',label:'API Gateway'}]},
|
||||
{name:'Observabilidade',tabs:[
|
||||
{key:'alarms',label:'Alarmes'},{key:'log_groups',label:'Log Groups'},
|
||||
{key:'notification_topics',label:'Notificações'},{key:'events_rules',label:'Events'}]},
|
||||
{name:'Segurança',tabs:[
|
||||
{key:'vaults',label:'Vaults'},{key:'dns_zones',label:'DNS'},
|
||||
{key:'network_firewalls',label:'Firewalls'},{key:'network_firewall_policies',label:'FW Policies'}]},
|
||||
{name:'IAM',tabs:[
|
||||
{key:'iam_users',label:'Usuários'},{key:'iam_groups',label:'Grupos'},
|
||||
{key:'iam_policies',label:'Policies'},{key:'iam_dynamic_groups',label:'Dynamic Groups'}]}
|
||||
];
|
||||
const EXP_TABS=EXP_GROUPS.flatMap(g=>g.tabs);
|
||||
function rExplorer(){
|
||||
const cfgSel=S.ociCfg.map(c=>`<option value="${c.id}">${c.tenancy_name} (${c.region})</option>`).join('');
|
||||
if(!S.ociCfg.length)return`<div class="cd"><div class="emp"><div class="eic">☁️</div><p>Nenhuma credencial OCI configurada.</p><p style="font-size:.74rem;margin-top:.35rem">Configure em <strong>Credenciais OCI</strong>.</p></div></div>`;
|
||||
return`<div class="cd"><div class="ct">🔍 Explorar Conta OCI</div>
|
||||
<div class="g3"><div class="ig"><label>Conexão OCI</label><select id="exCfg">${cfgSel}</select></div>
|
||||
<div class="ig"><label>Recurso</label><select id="exRes"><option value="compartments">Compartments</option><option value="regions">Regions</option><option value="instances">Compute Instances</option><option value="vcns">VCNs</option><option value="databases">Autonomous DBs</option><option value="buckets">Object Storage Buckets</option></select></div>
|
||||
<div class="ig"><label> </label><button class="btn bp" onclick="doExplore()">🔍 Explorar</button></div></div></div>
|
||||
<div class="cd"><div class="ct" id="exTitle">Resultados</div><div id="exOut">${S.expData?renderExpData(S.expData):'<div class="emp"><p>Selecione uma conexão e recurso.</p></div>'}</div></div>`}
|
||||
async function doExplore(){const cid=document.getElementById('exCfg').value;const res=document.getElementById('exRes').value;
|
||||
document.getElementById('exOut').innerHTML='<div class="ld"><span class="spinner"></span> Consultando OCI...</div>';
|
||||
try{const d=await $api('/oci/explore/'+cid+'/'+res);S.expData=d;
|
||||
document.getElementById('exTitle').textContent='Resultados — '+res;
|
||||
document.getElementById('exOut').innerHTML=renderExpData(d)}catch(e){document.getElementById('exOut').innerHTML='<div class="al al-e">'+e.message+'</div>'}}
|
||||
function renderExpData(d){if(d.error)return`<div class="al al-e">${d.error}</div>`;if(!Array.isArray(d)||!d.length)return'<div class="emp"><p>Nenhum resultado encontrado.</p></div>';
|
||||
if(!S.expCfg&&S.ociCfg.length)S.expCfg=S.ociCfg[0].id;
|
||||
const cfgOpts=S.ociCfg.map(c=>`<option value="${c.id}"${c.id===S.expCfg?' selected':''}>${c.tenancy_name} (${c.region})</option>`).join('');
|
||||
const regLabel=S.expSelRegions.length?S.expSelRegions.length+' região(ões)':'Todas as regiões';
|
||||
const regChecks=S.expRegions.map(r=>`<label style="display:flex;align-items:center;gap:.35rem;padding:.25rem .5rem;font-size:.72rem;cursor:pointer;border-radius:4px;transition:background .1s" onmouseover="this.style.background='var(--bg2)'" onmouseout="this.style.background=''"><input type="checkbox" ${S.expSelRegions.includes(r.name)?'checked':''} onchange="expToggleRegion('${r.name}')" style="accent-color:var(--ac)"> ${r.name}${r.is_home?' <span style="font-size:.58rem;color:var(--ac)">(Home)</span>':''}</label>`).join('');
|
||||
const catIcons={Compute:'💻',Rede:'🌐',Storage:'💾',Database:'🗄️',Containers:'📦',Serverless:'⚡',Observabilidade:'📊',Segurança:'🔒',IAM:'👤'};
|
||||
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('');
|
||||
return`<div class="exp-wrap">
|
||||
<div class="exp-tree" id="exp-tree"><div style="padding:.5rem .65rem .4rem;font-size:.74rem;font-weight:700;color:var(--t2);border-bottom:1px solid var(--bd);margin-bottom:.3rem;display:flex;align-items:center;gap:.35rem">📁 Compartments</div>${S.expTree.length?rExpTree(null,0):'<div class="ld"><span class="spinner"></span> Carregando...</div>'}</div>
|
||||
<div class="exp-resize" onmousedown="expStartResize(event)"></div>
|
||||
<div class="exp-main">
|
||||
<div class="exp-toolbar"><select onchange="expSetCfg(this.value)">${cfgOpts}</select>
|
||||
${S.expRegions.length?`<div class="dd" style="position:relative" onclick="event.stopPropagation()"><div onclick="S.expRegDdOpen=!S.expRegDdOpen;R()" style="display:flex;align-items:center;gap:.3rem;cursor:pointer;padding:.3rem .6rem;background:var(--bg2);border:1px solid var(--bd);border-radius:8px;font-size:.72rem;white-space:nowrap;min-width:130px;justify-content:space-between"><span>🌍 ${regLabel}</span><span style="font-size:.55rem;color:var(--t4)">▼</span></div>
|
||||
${S.expRegDdOpen?`<div style="position:absolute;top:100%;left:0;margin-top:4px;background:var(--bg);border:1px solid var(--bd);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.12);z-index:60;min-width:220px;overflow:hidden">
|
||||
<div style="padding:.5rem .6rem;border-bottom:1px solid var(--bd);display:flex;justify-content:space-between;align-items:center"><span style="font-size:.7rem;font-weight:600;color:var(--t2)">Regiões</span><div style="display:flex;gap:.3rem"><button class="btn bs" style="font-size:.6rem;padding:.15rem .4rem" onclick="S.expSelRegions=S.expRegions.map(r=>r.name);R()">Todas</button><button class="btn bs" style="font-size:.6rem;padding:.15rem .4rem" onclick="S.expSelRegions=[];R()">Nenhuma</button></div></div>
|
||||
<div style="max-height:240px;overflow-y:auto;padding:.3rem 0">${regChecks}</div>
|
||||
<div style="padding:.4rem .6rem;border-top:1px solid var(--bd);text-align:right"><button class="btn bp" style="font-size:.68rem;padding:.25rem .7rem" onclick="S.expRegDdOpen=false;expApplyRegions()">Aplicar</button></div>
|
||||
</div>`:''}</div>`:''}
|
||||
${S.expSelRegions.length?`<div style="display:flex;gap:.25rem;flex-wrap:wrap;align-items:center">${S.expSelRegions.map(r=>`<span class="tag" style="font-size:.6rem;display:inline-flex;align-items:center;gap:.2rem;background:var(--acl);color:var(--ac);border:1px solid var(--ac);border-radius:6px;padding:.1rem .35rem">${r}<span onclick="expRemoveRegion('${r}')" style="cursor:pointer;font-weight:700;font-size:.7rem">×</span></span>`).join('')}</div>`:''}
|
||||
<button class="btn" style="margin-left:auto;font-size:.7rem;padding:.25rem .6rem" onclick="expRefresh()">↻ Refresh</button></div>
|
||||
<div class="exp-nav"><div class="exp-cats">${cats}</div></div>
|
||||
<div class="exp-tabs">${tabs}</div>
|
||||
<div class="exp-content">${S.expLoading?'<div class="ld"><span class="spinner"></span> Consultando OCI...</div>':renderExpData(S.expData)}</div>
|
||||
</div></div>`}
|
||||
function rExpTree(parentId,depth){
|
||||
const children=S.expTree.filter(n=>n.parent_id===parentId);
|
||||
if(!children.length)return'';
|
||||
return children.map(n=>{
|
||||
const hasKids=S.expTree.some(c=>c.parent_id===n.id);
|
||||
const isOpen=S.expTreeOpen[n.id];
|
||||
const isSel=S.expSelComp===n.id;
|
||||
return`<div class="exp-node" style="padding-left:${depth*0.6}rem">
|
||||
<div class="exp-node-row${isSel?' selected':''}" onclick="expSelectComp('${n.id}')">
|
||||
<span class="exp-node-toggle" onclick="event.stopPropagation();expToggleNode('${n.id}')">${hasKids?(isOpen?'▼':'▶'):' '}</span>
|
||||
<span>${n.name}</span></div>
|
||||
${hasKids&&isOpen?`<div class="exp-node-children">${rExpTree(n.id,depth+1)}</div>`:''}</div>`}).join('')}
|
||||
async function expSetCfg(cid){S.expCfg=cid;S.expTree=[];S.expSelComp='';S.expData=null;S.expSelRegions=[];S.expRegions=[];S.expCounts={};S.expRegDdOpen=false;R();await expLoadTree();await expLoadRegions()}
|
||||
function expToggleRegion(r){const i=S.expSelRegions.indexOf(r);if(i>=0)S.expSelRegions.splice(i,1);else S.expSelRegions.push(r);R()}
|
||||
function expRemoveRegion(r){S.expSelRegions=S.expSelRegions.filter(x=>x!==r);if(S.expSelComp)expLoadRes()}
|
||||
async function expApplyRegions(){if(S.expSelComp)await expLoadRes()}
|
||||
function expSetCat(c){S.expCat=c;const grp=EXP_GROUPS.find(g=>g.name===c);if(grp&&grp.tabs.length){S.expResType=grp.tabs[0].key;S.expData=null;R();if(S.expSelComp)expLoadRes()}else R()}
|
||||
function expSetTab(t){S.expResType=t;S.expData=null;R();if(S.expSelComp)expLoadRes()}
|
||||
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 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()}}
|
||||
async function expLoadTree(){
|
||||
if(!S.expCfg)return;
|
||||
try{const d=await $api('/oci/explore/'+S.expCfg+'/compartment-tree');
|
||||
S.expTree=d.error?[]:(d||[]);
|
||||
if(S.expTree.length&&!S.expSelComp){S.expSelComp=S.expTree[0].id;S.expTreeOpen[S.expTree[0].id]=true}
|
||||
R();if(S.expSelComp)await expLoadRes()}catch(e){S.expTree=[];R()}}
|
||||
async function expLoadRegions(){
|
||||
if(!S.expCfg)return;
|
||||
try{const d=await $api('/oci/explore/'+S.expCfg+'/regions');S.expRegions=d.error?[]:(d||[]);R()}catch(e){S.expRegions=[];R()}}
|
||||
async function expLoadRes(){
|
||||
if(!S.expCfg||!S.expSelComp)return;
|
||||
S.expLoading=true;S._expExpanded={};R();
|
||||
const regions=S.expSelRegions.length?S.expSelRegions:[null];
|
||||
let all=[];let errors=[];
|
||||
const BATCH=3;const DELAY=800;
|
||||
for(let i=0;i<regions.length;i+=BATCH){
|
||||
const batch=regions.slice(i,i+BATCH);
|
||||
const fetches=batch.map(async reg=>{
|
||||
const q=new URLSearchParams({compartment_id:S.expSelComp});
|
||||
if(reg)q.set('region',reg);
|
||||
try{const d=await $api('/oci/explore/'+S.expCfg+'/'+S.expResType+'?'+q);
|
||||
if(d.error){errors.push(reg+': '+d.error)}
|
||||
else if(Array.isArray(d)){d.forEach(item=>{if(reg&&!item.region)item._region=reg});all=all.concat(d)}}
|
||||
catch(e){errors.push((reg||'default')+': '+e.message)}});
|
||||
await Promise.all(fetches);
|
||||
if(i+BATCH<regions.length)await new Promise(r=>setTimeout(r,DELAY))}
|
||||
if(all.length)S.expData=all;
|
||||
else if(errors.length)S.expData={error:errors.join('\n')};
|
||||
else S.expData=[];
|
||||
S.expCounts[S.expResType]=all.length;
|
||||
S.expLoading=false;R()}
|
||||
function renderExpData(d){if(!d)return'<div class="emp"><p>Selecione um compartment na árvore.</p></div>';
|
||||
if(d.error)return`<div class="al al-e">${d.error}</div>`;if(!Array.isArray(d)||!d.length)return'<div class="emp"><p>Nenhum recurso encontrado neste compartment.</p></div>';
|
||||
if(S.expResType==='security_lists')return rExpSecLists(d);
|
||||
if(S.expResType==='route_tables')return rExpRouteTables(d);
|
||||
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).filter(k=>k!=='id');
|
||||
const name=i.display_name||i.name||i.db_name||'—';
|
||||
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'];
|
||||
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>`:''}<br>
|
||||
${keys.filter(k=>!['display_name','name','db_name','lifecycle_state'].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>`}
|
||||
${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)=>{
|
||||
const rules=sl.rules||[];
|
||||
const ingress=rules.filter(r=>r.direction==='ingress');
|
||||
const egress=rules.filter(r=>r.direction==='egress');
|
||||
const expanded=S._expExpanded&&S._expExpanded[idx];
|
||||
return`<div class="exp-c" style="max-width:100%"><div style="display:flex;justify-content:space-between;align-items:center;cursor:pointer" onclick="S._expExpanded=S._expExpanded||{};S._expExpanded[${idx}]=!S._expExpanded[${idx}];R()">
|
||||
<div><strong style="display:inline">${sl.display_name}</strong> <span class="badge ${sl.lifecycle_state==='AVAILABLE'?'b-pass':'b-pend'}">${sl.lifecycle_state}</span></div>
|
||||
<div style="font-size:.66rem;color:var(--t4)">${expanded?'▼':'▶'} ${ingress.length} ingress · ${egress.length} egress</div></div>
|
||||
<div class="em" style="margin-top:.3rem">id: ${sl.id}</div>
|
||||
${expanded?`<div style="margin-top:.5rem">${ingress.length?`<div style="font-size:.68rem;font-weight:600;color:var(--t2);margin:.4rem 0 .2rem">↓ Ingress (${ingress.length})</div>
|
||||
<table style="width:100%;font-size:.66rem;border-collapse:collapse"><thead><tr style="background:var(--bg3);text-align:left"><th style="padding:.25rem .4rem">Protocolo</th><th style="padding:.25rem .4rem">Origem</th><th style="padding:.25rem .4rem">Portas</th><th style="padding:.25rem .4rem">Stateless</th><th style="padding:.25rem .4rem">Descrição</th></tr></thead><tbody>
|
||||
${ingress.map(r=>`<tr style="border-top:1px solid var(--bd)"><td style="padding:.25rem .4rem;font-weight:600">${r.protocol}</td><td style="padding:.25rem .4rem;font-family:var(--fm);font-size:.62rem">${r.source_dest}</td><td style="padding:.25rem .4rem">${r.ports||'ALL'}</td><td style="padding:.25rem .4rem">${r.stateless?'Sim':'Não'}</td><td style="padding:.25rem .4rem;color:var(--t4);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${(r.description||'').replace(/"/g,'"')}">${r.description||'—'}</td></tr>`).join('')}
|
||||
</tbody></table>`:''}
|
||||
${egress.length?`<div style="font-size:.68rem;font-weight:600;color:var(--t2);margin:.4rem 0 .2rem">↑ Egress (${egress.length})</div>
|
||||
<table style="width:100%;font-size:.66rem;border-collapse:collapse"><thead><tr style="background:var(--bg3);text-align:left"><th style="padding:.25rem .4rem">Protocolo</th><th style="padding:.25rem .4rem">Destino</th><th style="padding:.25rem .4rem">Portas</th><th style="padding:.25rem .4rem">Stateless</th><th style="padding:.25rem .4rem">Descrição</th></tr></thead><tbody>
|
||||
${egress.map(r=>`<tr style="border-top:1px solid var(--bd)"><td style="padding:.25rem .4rem;font-weight:600">${r.protocol}</td><td style="padding:.25rem .4rem;font-family:var(--fm);font-size:.62rem">${r.source_dest}</td><td style="padding:.25rem .4rem">${r.ports||'ALL'}</td><td style="padding:.25rem .4rem">${r.stateless?'Sim':'Não'}</td><td style="padding:.25rem .4rem;color:var(--t4);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${(r.description||'').replace(/"/g,'"')}">${r.description||'—'}</td></tr>`).join('')}
|
||||
</tbody></table>`:''}</div>`:''}</div>`}).join('')}`}
|
||||
function rExpRouteTables(d){
|
||||
return`<div style="font-size:.74rem;color:var(--t4);margin-bottom:.5rem">${d.length} route table(s)</div>${d.map((rt,idx)=>{
|
||||
const routes=rt.routes||[];
|
||||
const expanded=S._expExpanded&&S._expExpanded['rt'+idx];
|
||||
return`<div class="exp-c" style="max-width:100%"><div style="display:flex;justify-content:space-between;align-items:center;cursor:pointer" onclick="S._expExpanded=S._expExpanded||{};S._expExpanded['rt'+${idx}]=!S._expExpanded['rt'+${idx}];R()">
|
||||
<div><strong style="display:inline">${rt.display_name}</strong> <span class="badge ${rt.lifecycle_state==='AVAILABLE'?'b-pass':'b-pend'}">${rt.lifecycle_state}</span></div>
|
||||
<div style="font-size:.66rem;color:var(--t4)">${expanded?'▼':'▶'} ${routes.length} rota(s)</div></div>
|
||||
<div class="em" style="margin-top:.3rem">id: ${rt.id}</div>
|
||||
${expanded&&routes.length?`<div style="margin-top:.5rem">
|
||||
<table style="width:100%;font-size:.66rem;border-collapse:collapse"><thead><tr style="background:var(--bg3);text-align:left"><th style="padding:.25rem .4rem">Destino</th><th style="padding:.25rem .4rem">Tipo</th><th style="padding:.25rem .4rem">Target</th><th style="padding:.25rem .4rem">Descrição</th></tr></thead><tbody>
|
||||
${routes.map(r=>`<tr style="border-top:1px solid var(--bd)"><td style="padding:.25rem .4rem;font-family:var(--fm);font-size:.62rem;font-weight:600">${r.destination}</td><td style="padding:.25rem .4rem">${r.destination_type}</td><td style="padding:.25rem .4rem;font-size:.6rem;color:var(--t3)">${r.target_type}<br><span class="em">${r.target_id}</span></td><td style="padding:.25rem .4rem;color:var(--t4);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${(r.description||'').replace(/"/g,'"')}">${r.description||'—'}</td></tr>`).join('')}
|
||||
</tbody></table></div>`:expanded?'<div style="font-size:.7rem;color:var(--t4);padding:.4rem">Nenhuma rota configurada</div>':''}</div>`}).join('')}`}
|
||||
|
||||
/* ── Report ── */
|
||||
function rReport(){const comp=S.reports.filter(r=>r.status==='completed');
|
||||
|
||||
Reference in New Issue
Block a user