diff --git a/backend/app.py b/backend/app.py index 09e0a48..f5d0967 100644 --- a/backend/app.py +++ b/backend/app.py @@ -181,7 +181,20 @@ def init_db(): action TEXT NOT NULL, resource TEXT, details TEXT, ip TEXT, created_at TEXT DEFAULT (datetime('now')) ); + CREATE TABLE IF NOT EXISTS config_logs ( + id TEXT PRIMARY KEY, + config_type TEXT NOT NULL, + config_id TEXT NOT NULL, + config_name TEXT, + severity TEXT NOT NULL, + action TEXT NOT NULL, + message TEXT NOT NULL, + user_id TEXT, + username TEXT, + created_at TEXT DEFAULT (datetime('now')) + ); """) + c.execute("DELETE FROM config_logs WHERE created_at < datetime('now', '-30 days')") # ── Migrations ── for col in ["genai_config_id TEXT", "embedding_model_id TEXT DEFAULT 'cohere.embed-multilingual-v3.0'"]: try: @@ -241,6 +254,11 @@ def _audit(uid, uname, action, resource=None, details=None, ip=None): c.execute("INSERT INTO audit_log (id,user_id,username,action,resource,details,ip) VALUES (?,?,?,?,?,?,?)", (str(uuid.uuid4()), uid, uname, action, resource, details, ip)) +def _config_log(config_type, config_id, config_name, severity, action, message, uid=None, uname=None): + with db() as c: + c.execute("INSERT INTO config_logs (id,config_type,config_id,config_name,severity,action,message,user_id,username) VALUES (?,?,?,?,?,?,?,?,?)", + (str(uuid.uuid4()), config_type, config_id, config_name or "", severity, action, str(message)[:2000], uid, uname)) + # ── Models ──────────────────────────────────────────────────────────────────── class LoginReq(BaseModel): username: str; password: str; totp_code: Optional[str] = None @@ -391,6 +409,7 @@ async def save_oci( c.execute("INSERT INTO oci_configs (id,user_id,tenancy_name,tenancy_ocid,user_ocid,fingerprint,region,key_file_path,compartment_id) VALUES (?,?,?,?,?,?,?,?,?)", (cid, u["id"], tenancy_name, tenancy_ocid, user_ocid, fingerprint, region, str(kp), compartment_id or None)) _audit(u["id"], u["username"], "save_oci_config", cid, f"tenancy={tenancy_name}") + _config_log("oci", cid, tenancy_name, "success", "save", f"Credencial salva: {tenancy_name} ({region})", u["id"], u["username"]) return {"id": cid, "tenancy_name": tenancy_name, "region": region} @app.get("/api/oci/configs") @@ -414,14 +433,30 @@ async def del_oci(cid: str, u=Depends(require("admin","user"))): @app.post("/api/oci/test/{cid}") async def test_oci(cid: str, u=Depends(require("admin","user"))): cp = OCI_DIR / cid / "config" - if not cp.exists(): return {"status":"error","message":"Config não encontrada"} + cname = None + with db() as c: + row = c.execute("SELECT tenancy_name FROM oci_configs WHERE id=?", (cid,)).fetchone() + if row: cname = row["tenancy_name"] + if not cp.exists(): + _config_log("oci", cid, cname, "error", "test", "Config não encontrada", u["id"], u["username"]) + return {"status":"error","message":"Config não encontrada"} try: r = subprocess.run(["oci","iam","region","list","--config-file",str(cp),"--output","json"], capture_output=True, text=True, timeout=30) - if r.returncode == 0: return {"status":"success","message":"Conexão OK","data":json.loads(r.stdout)} - return {"status":"error","message":r.stderr[:500]} - except FileNotFoundError: return {"status":"error","message":"OCI CLI não disponível no container."} - except subprocess.TimeoutExpired: return {"status":"error","message":"Timeout na conexão"} - except Exception as e: return {"status":"error","message":str(e)} + if r.returncode == 0: + _config_log("oci", cid, cname, "success", "test", "Conexão OK", u["id"], u["username"]) + return {"status":"success","message":"Conexão OK","data":json.loads(r.stdout)} + msg = r.stderr[:500] + _config_log("oci", cid, cname, "error", "test", msg, u["id"], u["username"]) + return {"status":"error","message":msg} + except FileNotFoundError: + _config_log("oci", cid, cname, "error", "test", "OCI CLI não disponível no container.", u["id"], u["username"]) + return {"status":"error","message":"OCI CLI não disponível no container."} + except subprocess.TimeoutExpired: + _config_log("oci", cid, cname, "error", "test", "Timeout na conexão", u["id"], u["username"]) + return {"status":"error","message":"Timeout na conexão"} + except Exception as e: + _config_log("oci", cid, cname, "error", "test", str(e)[:500], u["id"], u["username"]) + return {"status":"error","message":str(e)[:500]} # ── OCI Account Explorer ────────────────────────────────────────────────────── @app.get("/api/oci/explore/{cid}/compartments") @@ -531,6 +566,7 @@ async def save_genai(req: GenAIConfigReq, u=Depends(require("admin","user"))): req.temperature, req.max_tokens, req.top_p, req.top_k, req.frequency_penalty, req.presence_penalty, int(req.is_default))) _audit(u["id"], u["username"], "save_genai_config", gid, req.model_id) + _config_log("genai", gid, req.name, "success", "save", f"Modelo salvo: {req.model_id} ({req.genai_region})", u["id"], u["username"]) return {"id": gid, "model_id": req.model_id, "endpoint": ep} @app.get("/api/genai/configs") @@ -550,11 +586,15 @@ async def test_genai(gid: str, u=Depends(require("admin","user"))): with db() as c: gc = c.execute("SELECT * FROM genai_configs WHERE id=?",(gid,)).fetchone() if not gc: raise HTTPException(404) + gname = gc["name"] try: resp = _call_genai(dict(gc), "Say 'connection successful' in one short sentence.") + _config_log("genai", gid, gname, "success", "test", f"GenAI OK: {resp[:200]}", u["id"], u["username"]) return {"status":"success","message":"GenAI OK","response":resp[:300]} except Exception as e: - return {"status":"error","message":str(e)[:500]} + msg = str(e)[:500] + _config_log("genai", gid, gname, "error", "test", msg, u["id"], u["username"]) + return {"status":"error","message":msg} def _call_genai(gc: dict, message: str, history: list = None) -> str: """ @@ -801,6 +841,7 @@ async def register_mcp(req: MCPServerReq, u=Depends(require("admin","user"))): json.dumps(req.env_vars) if req.env_vars else None, req.url, req.module_path, json.dumps(req.tools) if req.tools else None, req.linked_adb_id)) _audit(u["id"], u["username"], "register_mcp", mid, req.name) + _config_log("mcp", mid, req.name, "success", "save", f"MCP registrado: {req.name} ({req.server_type})", u["id"], u["username"]) return {"id": mid, "name": req.name, "server_type": req.server_type} @app.get("/api/mcp/servers") @@ -855,17 +896,28 @@ async def save_adb(req: ADBVectorReq, u=Depends(require("admin","user"))): _enc(req.wallet_password) if req.wallet_password else None, req.table_name, int(req.use_mtls), req.genai_config_id, req.embedding_model_id)) _audit(u["id"], u["username"], "save_adb_config", vid, req.config_name) + _config_log("adb", vid, req.config_name, "success", "save", f"Conexão salva: {req.config_name} ({req.dsn})", u["id"], u["username"]) return {"id": vid, "config_name": req.config_name} @app.post("/api/adb/{vid}/upload-wallet") async def upload_wallet(vid: str, wallet: UploadFile = File(...), u=Depends(require("admin","user"))): - wdir = WALLET_DIR / vid; wdir.mkdir(parents=True, exist_ok=True) - zp = wdir / "wallet.zip" - zp.write_bytes(await wallet.read()) - import zipfile - with zipfile.ZipFile(str(zp), 'r') as z: z.extractall(str(wdir)) - with db() as c: c.execute("UPDATE adb_vector_configs SET wallet_dir=? WHERE id=?", (str(wdir), vid)) - return {"ok": True, "wallet_dir": str(wdir), "files": os.listdir(str(wdir))} + cname = None + with db() as c: + row = c.execute("SELECT config_name FROM adb_vector_configs WHERE id=?", (vid,)).fetchone() + if row: cname = row["config_name"] + try: + wdir = WALLET_DIR / vid; wdir.mkdir(parents=True, exist_ok=True) + zp = wdir / "wallet.zip" + zp.write_bytes(await wallet.read()) + import zipfile + with zipfile.ZipFile(str(zp), 'r') as z: z.extractall(str(wdir)) + with db() as c: c.execute("UPDATE adb_vector_configs SET wallet_dir=? WHERE id=?", (str(wdir), vid)) + files = os.listdir(str(wdir)) + _config_log("adb", vid, cname, "success", "upload", f"Wallet enviada: {', '.join(files)}", u["id"], u["username"]) + return {"ok": True, "wallet_dir": str(wdir), "files": files} + except Exception as e: + _config_log("adb", vid, cname, "error", "upload", str(e)[:500], u["id"], u["username"]) + raise HTTPException(500, str(e)[:500]) @app.get("/api/adb/configs") async def list_adb(u=Depends(current_user)): @@ -880,14 +932,19 @@ async def test_adb(vid: str, u=Depends(require("admin","user"))): with db() as c: cfg = c.execute("SELECT * FROM adb_vector_configs WHERE id=?",(vid,)).fetchone() if not cfg: raise HTTPException(404) + cname = cfg["config_name"] try: conn = _get_adb_connection(dict(cfg)) cur = conn.cursor(); cur.execute("SELECT 1 FROM DUAL"); cur.close(); conn.close() + _config_log("adb", vid, cname, "success", "test", "Conexão Autonomous DB OK!", u["id"], u["username"]) return {"status":"success","message":"Conexão Autonomous DB OK!"} except ImportError: + _config_log("adb", vid, cname, "error", "test", "python-oracledb não disponível no container.", u["id"], u["username"]) return {"status":"error","message":"python-oracledb não disponível no container."} except Exception as e: - return {"status":"error","message":str(e)[:500]} + msg = str(e)[:500] + _config_log("adb", vid, cname, "error", "test", msg, u["id"], u["username"]) + return {"status":"error","message":msg} @app.delete("/api/adb/configs/{vid}") async def del_adb(vid: str, u=Depends(require("admin","user"))): @@ -948,8 +1005,10 @@ def _ingest_documents_task(cfg: dict, genai_cfg: dict, documents: list, user_id: cur.close() log.info(f"Ingested {inserted}/{len(documents)} documents into {table_name}") _audit(user_id, username, "ingest_documents", cfg["id"], f"{inserted} documents") + _config_log("adb", cfg["id"], cfg.get("config_name"), "success", "ingest", f"{inserted}/{len(documents)} documentos ingeridos em {table_name}", user_id, username) except Exception as e: log.error(f"Ingestion task failed: {e}") + _config_log("adb", cfg["id"], cfg.get("config_name"), "error", "ingest", str(e)[:500], user_id, username) finally: conn.close() @@ -1124,12 +1183,15 @@ async def _exec_report(rid, cfg, regions, mcp_server_id): if proc.returncode == 0 and jp.exists(): c.execute("UPDATE reports SET status='completed',report_data=?,html_path=?,json_path=?,completed_at=datetime('now') WHERE id=?", (jp.read_text()[:500_000], str(hp) if hp.exists() else None, str(jp), rid)) + _config_log("oci", cfg["id"], cfg["tenancy_name"], "success", "report", f"Relatório concluído: {rid}") else: - c.execute("UPDATE reports SET status='failed',error_msg=?,completed_at=datetime('now') WHERE id=?", - ((stderr.decode() if stderr else "Unknown")[:2000], rid)) + err = (stderr.decode() if stderr else "Unknown")[:2000] + c.execute("UPDATE reports SET status='failed',error_msg=?,completed_at=datetime('now') WHERE id=?", (err, rid)) + _config_log("oci", cfg["id"], cfg["tenancy_name"], "error", "report", err) except Exception as e: with db() as c: c.execute("UPDATE reports SET status='failed',error_msg=?,completed_at=datetime('now') WHERE id=?", (str(e)[:2000], rid)) + _config_log("oci", cfg["id"], cfg["tenancy_name"], "error", "report", str(e)[:2000]) @app.get("/api/reports") async def list_reports(u=Depends(current_user)): @@ -1270,6 +1332,36 @@ async def audit_log(limit:int=Query(100,le=500), u=Depends(require("admin"))): with db() as c: rows=c.execute("SELECT * FROM audit_log ORDER BY created_at DESC LIMIT ?",(limit,)).fetchall() return [dict(r) for r in rows] +# ── Config Logs ─────────────────────────────────────────────────────────────── +@app.get("/api/config-logs") +async def get_config_logs( + config_type: str = Query(None), config_id: str = Query(None), + severity: str = Query(None), limit: int = Query(50, le=200), + u=Depends(current_user) +): + query = "SELECT * FROM config_logs WHERE 1=1" + params = [] + if config_type: query += " AND config_type=?"; params.append(config_type) + if config_id: query += " AND config_id=?"; params.append(config_id) + if severity: query += " AND severity=?"; params.append(severity) + if u["role"] != "admin": query += " AND user_id=?"; params.append(u["id"]) + query += " ORDER BY created_at DESC LIMIT ?" + params.append(limit) + with db() as c: rows = c.execute(query, params).fetchall() + return [dict(r) for r in rows] + +@app.delete("/api/config-logs") +async def clear_config_logs( + config_type: str = Query(None), config_id: str = Query(None), + u=Depends(require("admin")) +): + query = "DELETE FROM config_logs WHERE 1=1" + params = [] + if config_type: query += " AND config_type=?"; params.append(config_type) + if config_id: query += " AND config_id=?"; params.append(config_id) + with db() as c: c.execute(query, params) + return {"ok": True} + # ── Health ──────────────────────────────────────────────────────────────────── @app.get("/api/health") async def health(): diff --git a/frontend/index.html b/frontend/index.html index 98fff9e..ac6454d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -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`
@@ -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`
☁️ Credenciais Registradas
+function rOci(){return`
☁️ Credenciais Registradas
${S.ociCfg.map(c=>` @@ -347,7 +348,8 @@ ${!S.ociCfg.length?'

Nenhuma credencia

-`} + +`+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',' 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?'

Nenhum modelo c

➕ Novo Modelo GenAI
Configura conexão via OCI SDK (GenerativeAiInferenceClient). O endpoint é gerado automaticamente pela região selecionada.
-
+
@@ -385,7 +387,8 @@ ${S.ociCfg.map(c=>``).join('')
-
`} +
+`+rConfigLogs('genai')} async function sGenai(){const btn=document.getElementById('gbtn');btn.disabled=true;btn.textContent='Salvando...';sm('gm',' 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'+cfg.tenancy_name+'','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?`
Módulo:
Opcional — o MCP server poderá usar este banco como ferramenta.
-`} + +`+rConfigLogs('mcp')} async function sMcp(){const btn=document.getElementById('mcbtn');btn.disabled=true;btn.textContent='Registrando...';sm('mcm',' 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?'

Nenhuma conexão

📤 Upload Wallet
-
`} +
+`+rConfigLogs('adb')} async function sAdb(){const btn=document.getElementById('abtn');btn.disabled=true;btn.textContent='Salvando...';sm('am',' 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',' 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=`
TenancyRegionCompartmentCriadoAções
${c.tenancy_name}${c.region} ${c.compartment_id?(c.compartment_id.slice(0,22)+'…'):'—'}${c.created_at}
`).join('')}
DataUsu ${logs.map(l=>`
${l.created_at}${l.username||'—'}${l.action} ${l.resource?(l.resource.slice(0,14)+'…'):'—'}${l.ip||'—'}
`}catch(e){document.getElementById('ac').innerHTML='
'+e.message+'
'}} +/* ── Config Logs ── */ +function rConfigLogs(type){return`
+📜 Log de Atividades +
+ +
+
Carregando logs...
`} + +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='

Nenhum log registrado.

';return} + el.innerHTML=''+ + logs.map(l=>{const sc=l.severity==='error'?'rd':l.severity==='success'?'gn':'bl'; + return''+ + ''+ + ''+ + ''+ + ''}).join('')+ + '
DataConfigAçãoStatusMensagem
'+l.created_at+''+(l.config_name||l.config_id?.slice(0,8)||'—')+''+l.action+''+l.severity+''+(l.message?.slice(0,150)||'—')+'
'} + catch(e){const el=document.getElementById('cl-'+type);if(el)el.innerHTML='
'+e.message+'
'}} + /* ── Helpers ── */ function sm(id,msg,t){const el=document.getElementById(id);if(el)el.innerHTML=`
${msg}
`} let _lu='',_lp='';