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:
126
backend/app.py
126
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():
|
||||
|
||||
@@ -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,'"')+'">'+(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='';
|
||||
|
||||
Reference in New Issue
Block a user