feat: add persistent config logs panel and auto-fill GenAI from OCI credentials

Backend: new config_logs table with _config_log() helper, instrumented across
all test/save/upload/report/ingest endpoints (OCI, GenAI, ADB, MCP). Added
GET/DELETE /api/config-logs with filtering by type, severity, and auto-cleanup
of logs older than 30 days.

Frontend: inline "Log de Atividades" panel at the bottom of each config tab
(OCI, GenAI, ADB, MCP) with severity filter dropdown. Replaced all remaining
alert() calls in test functions with inline sm() messages. GenAI config now
auto-fills Region and Compartment OCID when selecting an OCI credential.
This commit is contained in:
nogueiraguh
2026-03-03 15:21:38 -03:00
parent edc4652b35
commit 5a28f0d56f
2 changed files with 155 additions and 29 deletions

View File

@@ -213,7 +213,8 @@ async function loadData(){try{
if(S.user.role==='admin'){try{S.users=await $api('/users')}catch(e){}}
}catch(e){console.error(e)}}
function R(){document.getElementById('app').innerHTML=S.user?rApp():rLogin();bind()}
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;S.expData=null;R();if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl();
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">
@@ -330,7 +331,7 @@ function dlRpt(id,f){window.open(API+'/reports/'+id+'/download?fmt='+f,'_blank')
async function refreshDl(){S.reports=await $api('/reports');R()}
/* ── OCI Config ── */
function rOci(){return`<div class="cd"><div class="ct">☁️ Credenciais Registradas</div>
function rOci(){return`<div class="cd"><div class="ct">☁️ Credenciais Registradas</div><div id="ocm"></div>
<table><thead><tr><th>Tenancy</th><th>Region</th><th>Compartment</th><th>Criado</th><th>Ações</th></tr></thead><tbody>
${S.ociCfg.map(c=>`<tr><td><strong>${c.tenancy_name}</strong></td><td><span class="tag">${c.region}</span></td>
<td style="font-family:var(--fm);font-size:.66rem;color:var(--t4)">${c.compartment_id?(c.compartment_id.slice(0,22)+'…'):'—'}</td><td style="font-size:.72rem">${c.created_at}</td>
@@ -347,7 +348,8 @@ ${!S.ociCfg.length?'<div class="emp" style="padding:.85rem"><p>Nenhuma credencia
<div class="ig"><label>Compartment OCID</label><input type="text" id="ocp" placeholder="ocid1.compartment.oc1.."></div></div>
<div class="g2"><div class="ig"><label>Private Key (.pem)</label><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>
<button class="btn bp" id="obtn" onclick="sOci()">Salvar Credencial</button></div>`}
<button class="btn bp" id="obtn" onclick="sOci()">Salvar Credencial</button></div>
`+rConfigLogs('oci')}
async function sOci(){const fd=new FormData();fd.append('tenancy_name',document.getElementById('otn').value);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',document.getElementById('org').value);fd.append('compartment_id',document.getElementById('ocp').value);
@@ -355,7 +357,7 @@ async function sOci(){const fd=new FormData();fd.append('tenancy_name',document.
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{await $api('/oci/config',{method:'POST',body:fd,headers:{}});S.ociCfg=await $api('/oci/configs');sm('om','✅ Credencial salva com sucesso!','s');R()}catch(e){sm('om',e.message,'e');btn.disabled=false;btn.textContent='Salvar Credencial'}}
async function tOci(id){try{const d=await $api('/oci/test/'+id,{method:'POST'});alert(d.status==='success'?'✅ Conexão OK!':'❌ '+d.message)}catch(e){alert(e.message)}}
async function tOci(id){try{const d=await $api('/oci/test/'+id,{method:'POST'});sm('ocm',d.status==='success'?'✅ Conexão OK!':'❌ '+d.message,d.status==='success'?'s':'e');refreshCLogs('oci')}catch(e){sm('ocm',e.message,'e')}}
async function dOci(id){if(!confirm('Excluir credencial?'))return;await $api('/oci/configs/'+id,{method:'DELETE'});S.ociCfg=await $api('/oci/configs');R()}
/* ── GenAI Config ── */
@@ -372,8 +374,8 @@ ${!S.genaiCfg.length?'<div class="emp" style="padding:.85rem"><p>Nenhum modelo c
<div class="cd"><div class="ct"> Novo Modelo GenAI</div>
<div class="cdesc">Configura conexão via OCI SDK (<code style="font-size:.72rem">GenerativeAiInferenceClient</code>). O endpoint é gerado automaticamente pela região selecionada.</div><div id="gm"></div>
<div class="g3"><div class="ig"><label>Nome da Config</label><input type="text" id="gname" placeholder="Llama Produção"></div>
<div class="ig"><label>Credencial OCI</label><select id="goci"><option value="">Selecione...</option>
${S.ociCfg.map(c=>`<option value="${c.id}">${c.tenancy_name}</option>`).join('')}</select></div>
<div class="ig"><label>Credencial OCI</label><select id="goci" onchange="fillGenaiFromOci()"><option value="">Selecione...</option>
${S.ociCfg.map(c=>`<option value="${c.id}">${c.tenancy_name} (${c.region})</option>`).join('')}</select></div>
<div class="ig"><label>Modelo</label><select id="gmod">${mOpts}</select></div></div>
<div class="g3"><div class="ig"><label>Região GenAI</label><select id="greg">${rOpts}</select></div>
<div class="ig"><label>Compartment OCID</label><input type="text" id="gcmp" placeholder="ocid1.compartment.oc1.."></div>
@@ -385,7 +387,8 @@ ${S.ociCfg.map(c=>`<option value="${c.id}">${c.tenancy_name}</option>`).join('')
<div class="g3"><div class="ig"><label>Top K</label><input type="number" id="gtk" value="1" min="-1" max="500"></div>
<div class="ig"><label>Frequency Penalty</label><input type="number" id="gfp" value="0" step="0.1" min="0" max="2"></div>
<div class="ig"><label>Presence Penalty</label><input type="number" id="gpp" value="0" step="0.1" min="0" max="2"></div></div>
<button class="btn bp" id="gbtn" onclick="sGenai()">Salvar Modelo</button></div>`}
<button class="btn bp" id="gbtn" onclick="sGenai()">Salvar Modelo</button></div>
`+rConfigLogs('genai')}
async function sGenai(){const btn=document.getElementById('gbtn');btn.disabled=true;btn.textContent='Salvando...';sm('gm','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Salvando modelo...','i');try{await $api('/genai/config',{method:'POST',body:{
name:document.getElementById('gname').value||'default',
oci_config_id:document.getElementById('goci').value,model_id:document.getElementById('gmod').value,
@@ -397,7 +400,11 @@ async function sGenai(){const btn=document.getElementById('gbtn');btn.disabled=t
frequency_penalty:parseFloat(document.getElementById('gfp').value),presence_penalty:parseFloat(document.getElementById('gpp').value),
is_default:S.genaiCfg.length===0}});
S.genaiCfg=await $api('/genai/configs');sm('gm','✅ Modelo salvo com sucesso!','s');R()}catch(e){sm('gm',e.message,'e');btn.disabled=false;btn.textContent='Salvar Modelo'}}
async function tGenai(id){try{const d=await $api('/genai/test/'+id,{method:'POST'});alert(d.status==='success'?'✅ '+d.message+'\n'+d.response:'❌ '+d.message)}catch(e){alert(e.message)}}
function fillGenaiFromOci(){const sel=document.getElementById('goci');const cfg=S.ociCfg.find(c=>c.id===sel.value);if(!cfg)return;
const greg=document.getElementById('greg');if(greg){for(let i=0;i<greg.options.length;i++){if(greg.options[i].value===cfg.region){greg.selectedIndex=i;break}}}
if(cfg.compartment_id)document.getElementById('gcmp').value=cfg.compartment_id;
sm('gm','Região e Compartment preenchidos da credencial <strong>'+cfg.tenancy_name+'</strong>','i')}
async function tGenai(id){try{const d=await $api('/genai/test/'+id,{method:'POST'});sm('gm',d.status==='success'?'✅ '+d.message+(d.response?' — '+d.response:''):'❌ '+d.message,d.status==='success'?'s':'e');refreshCLogs('genai')}catch(e){sm('gm',e.message,'e')}}
async function dGenai(id){if(!confirm('Excluir modelo?'))return;await $api('/genai/configs/'+id,{method:'DELETE'});S.genaiCfg=await $api('/genai/configs');R()}
/* ── MCP Servers ── */
@@ -450,7 +457,8 @@ ${m.module_path?`<div><span style="color:var(--t3);font-weight:600">Módulo:</sp
<input type="text" id="margs" placeholder='["--config", "/path/to/config"]'></div></div>
<div class="ig" style="margin-top:.5rem"><label>🗄️ Vincular ADB Vector</label><div class="ht">Opcional — o MCP server poderá usar este banco como ferramenta.</div>
<select id="madb"><option value="">Nenhum</option>${adbOpts}</select></div>
<button class="btn bp" id="mcbtn" style="margin-top:.75rem" onclick="sMcp()">Registrar Server</button></div>`}
<button class="btn bp" id="mcbtn" style="margin-top:.75rem" onclick="sMcp()">Registrar Server</button></div>
`+rConfigLogs('mcp')}
async function sMcp(){const btn=document.getElementById('mcbtn');btn.disabled=true;btn.textContent='Registrando...';sm('mcm','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Registrando servidor...','i');try{let args=null;const a=document.getElementById('margs')?.value;if(a)try{args=JSON.parse(a)}catch(e){}
const mt=document.querySelector('input[name="mt"]:checked')?.value||'stdio';
await $api('/mcp/servers',{method:'POST',body:{name:document.getElementById('mn').value,description:document.getElementById('md').value,
@@ -491,20 +499,21 @@ ${!S.adbCfg.length?'<div class="emp" style="padding:.85rem"><p>Nenhuma conexão
<div class="cd"><div class="ct">📤 Upload Wallet</div><div id="wm"></div>
<div class="g2"><div class="ig"><label>Conexão ADB</label><select id="awsel">${S.adbCfg.map(c=>`<option value="${c.id}">${c.config_name}</option>`).join('')}</select></div>
<div class="ig"><label>Wallet ZIP</label><input type="file" id="awf" accept=".zip"></div></div>
<button class="btn bs" id="wbtn" onclick="uWallet()">Upload Wallet</button></div>`}
<button class="btn bs" id="wbtn" onclick="uWallet()">Upload Wallet</button></div>
`+rConfigLogs('adb')}
async function sAdb(){const btn=document.getElementById('abtn');btn.disabled=true;btn.textContent='Salvando...';sm('am','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Salvando conexão...','i');try{await $api('/adb/config',{method:'POST',body:{config_name:document.getElementById('an').value,dsn:document.getElementById('adsn').value,
username:document.getElementById('auser').value,password:document.getElementById('apw').value,
wallet_password:document.getElementById('awpw').value||null,table_name:document.getElementById('atbl').value,
genai_config_id:document.getElementById('agcfg').value||null,
embedding_model_id:document.getElementById('aemb').value||'cohere.embed-multilingual-v3.0'}});
S.adbCfg=await $api('/adb/configs');sm('am','✅ Conexão salva!','s');R()}catch(e){sm('am',e.message,'e');btn.disabled=false;btn.textContent='Salvar Conexão'}}
async function tAdb(id){try{const d=await $api('/adb/test/'+id,{method:'POST'});alert(d.status==='success'?'✅ '+d.message:'❌ '+d.message)}catch(e){alert(e.message)}}
async function tAdb(id){try{const d=await $api('/adb/test/'+id,{method:'POST'});sm('am',d.status==='success'?'✅ '+d.message:'❌ '+d.message,d.status==='success'?'s':'e');refreshCLogs('adb')}catch(e){sm('am',e.message,'e')}}
async function dAdb(id){if(!confirm('Excluir conexão?'))return;await $api('/adb/configs/'+id,{method:'DELETE'});S.adbCfg=await $api('/adb/configs');R()}
async function uWallet(){const fd=new FormData();const f=document.getElementById('awf').files[0];if(!f)return sm('wm','Selecione um arquivo wallet ZIP.','e');
fd.append('wallet',f);const id=document.getElementById('awsel').value;if(!id)return sm('wm','Selecione uma conexão ADB.','e');
const btn=document.getElementById('wbtn');btn.disabled=true;btn.textContent='Enviando...';sm('wm','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Enviando wallet...','i');
try{const d=await $api('/adb/'+id+'/upload-wallet',{method:'POST',body:fd,headers:{}});S.adbCfg=await $api('/adb/configs');sm('wm','✅ Wallet enviada! Arquivos: '+d.files.join(', '),'s');R()}catch(e){sm('wm',e.message,'e');btn.disabled=false;btn.textContent='Upload Wallet'}}
async function ensureTable(id){try{const d=await $api('/adb/'+id+'/ensure-table',{method:'POST'});alert('Tabela criada/verificada: '+d.table)}catch(e){alert('Erro: '+e.message)}}
async function ensureTable(id){try{const d=await $api('/adb/'+id+'/ensure-table',{method:'POST'});sm('am','✅ Tabela criada/verificada: '+d.table,'s')}catch(e){sm('am','Erro: '+e.message,'e')}}
/* ── Embeddings ── */
function rEmbeddings(){
@@ -646,6 +655,31 @@ document.getElementById('ac').innerHTML=`<table><thead><tr><th>Data</th><th>Usu
${logs.map(l=>`<tr><td style="font-size:.68rem;color:var(--t4)">${l.created_at}</td><td>${l.username||'—'}</td><td><span class="tag">${l.action}</span></td>
<td style="font-size:.68rem;font-family:var(--fm);color:var(--t4)">${l.resource?(l.resource.slice(0,14)+'…'):'—'}</td><td style="font-size:.72rem">${l.ip||'—'}</td></tr>`).join('')}</tbody></table>`}catch(e){document.getElementById('ac').innerHTML='<div class="al al-e">'+e.message+'</div>'}}
/* ── Config Logs ── */
function rConfigLogs(type){return`<div class="cd"><div class="ct" style="display:flex;justify-content:space-between;align-items:center">
<span>📜 Log de Atividades</span>
<div style="display:flex;gap:.4rem;align-items:center">
<select id="cl-sev-${type}" onchange="refreshCLogs('${type}')" style="font-size:.7rem;padding:.22rem .4rem;border-radius:6px;border:1px solid var(--bd)">
<option value="">Todos</option><option value="error">Erros</option><option value="success">Sucesso</option><option value="info">Info</option></select>
<button class="btn bs bsm" onclick="refreshCLogs('${type}')">Atualizar</button></div></div>
<div id="cl-${type}"><div style="padding:.75rem;font-size:.74rem;color:var(--t4)">Carregando logs...</div></div></div>`}
async function refreshCLogs(type){
const sev=document.getElementById('cl-sev-'+type)?.value||'';
const qs='config_type='+type+'&limit=30'+(sev?'&severity='+sev:'');
try{const logs=await $api('/config-logs?'+qs);
const el=document.getElementById('cl-'+type);if(!el)return;
if(!logs.length){el.innerHTML='<div class="emp" style="padding:.65rem"><p>Nenhum log registrado.</p></div>';return}
el.innerHTML='<table style="font-size:.74rem"><thead><tr><th>Data</th><th>Config</th><th>Ação</th><th>Status</th><th>Mensagem</th></tr></thead><tbody>'+
logs.map(l=>{const sc=l.severity==='error'?'rd':l.severity==='success'?'gn':'bl';
return'<tr><td style="font-size:.62rem;color:var(--t4);white-space:nowrap">'+l.created_at+'</td>'+
'<td style="font-size:.72rem">'+(l.config_name||l.config_id?.slice(0,8)||'—')+'</td>'+
'<td><span class="tag">'+l.action+'</span></td>'+
'<td><span class="badge" style="background:var(--'+sc+'l);color:var(--'+sc+')">'+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,'&quot;')+'">'+(l.message?.slice(0,150)||'—')+'</td></tr>'}).join('')+
'</tbody></table>'}
catch(e){const el=document.getElementById('cl-'+type);if(el)el.innerHTML='<div class="al al-e">'+e.message+'</div>'}}
/* ── Helpers ── */
function sm(id,msg,t){const el=document.getElementById(id);if(el)el.innerHTML=`<div class="al al-${t==='s'?'s':t==='i'?'i':'e'}">${msg}</div>`}
let _lu='',_lp='';