feat: add in-place config editing, OpenAI GPT-oss models, and Cohere Embed v4.0
- Add PUT endpoints for editing OCI, GenAI, ADB, and MCP configurations - Add edit buttons in all config tables with inline form pre-fill - Add OpenAI GPT-oss 120B and 20B models to GenAI catalog - Add Cohere Embed v4.0 (Multimodal, 1536d) to embedding catalog - Update OCI list endpoint to return user_ocid and fingerprint for editing - Add S.editing state management with cancel support - Bump version to 1.4
This commit is contained in:
13
README.md
13
README.md
@@ -9,7 +9,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/version-1.3-C74634?style=flat-square" alt="Version">
|
||||
<img src="https://img.shields.io/badge/version-1.4-C74634?style=flat-square" alt="Version">
|
||||
<img src="https://img.shields.io/badge/python-3.12-3776AB?style=flat-square" alt="Python">
|
||||
<img src="https://img.shields.io/badge/FastAPI-0.115-009688?style=flat-square" alt="FastAPI">
|
||||
<img src="https://img.shields.io/badge/OCI-GenAI-C74634?style=flat-square" alt="OCI">
|
||||
@@ -32,7 +32,7 @@ The platform combines security compliance scanning, AI-powered chat with **RAG (
|
||||
### 🤖 AI Chat Agent with RAG
|
||||
- **OCI Generative AI** integration via official SDK (`oci.generative_ai_inference`)
|
||||
- **RAG (Retrieval-Augmented Generation)**: automatically queries ADB vector store for relevant context before generating responses
|
||||
- 12 models across 4 providers: **Cohere** (Command A/R/R+), **Meta** (Llama 4/3.3/3.2/3.1), **Google** (Gemini 2.5), **xAI** (Grok 3/4)
|
||||
- 14 models across 5 providers: **Cohere** (Command A/R/R+), **Meta** (Llama 4/3.3/3.2/3.1), **Google** (Gemini 2.5), **xAI** (Grok 3/4), **OpenAI** (GPT-oss 120B/20B)
|
||||
- 16 OCI regions supported with auto-generated endpoints
|
||||
- Full parameter control: temperature, max_tokens, top_p, top_k, frequency/presence penalty
|
||||
- Conversation history with session management
|
||||
@@ -309,6 +309,7 @@ oci-cis-agent/
|
||||
|--------|----------|-------------|
|
||||
| POST | `/api/oci/config` | Save OCI credentials (multipart) |
|
||||
| GET | `/api/oci/configs` | List OCI credentials |
|
||||
| PUT | `/api/oci/configs/{id}` | Update OCI credential (multipart) |
|
||||
| POST | `/api/oci/test/{id}` | Test OCI connection |
|
||||
| DELETE | `/api/oci/configs/{id}` | Delete OCI credential |
|
||||
|
||||
@@ -330,6 +331,7 @@ oci-cis-agent/
|
||||
| GET | `/api/genai/models` | List available models and regions |
|
||||
| POST | `/api/genai/config` | Save GenAI configuration |
|
||||
| GET | `/api/genai/configs` | List GenAI configurations |
|
||||
| PUT | `/api/genai/configs/{id}` | Update GenAI configuration |
|
||||
| POST | `/api/genai/test/{id}` | Test GenAI connection |
|
||||
| DELETE | `/api/genai/configs/{id}` | Delete GenAI config |
|
||||
|
||||
@@ -339,6 +341,7 @@ oci-cis-agent/
|
||||
|--------|----------|-------------|
|
||||
| POST | `/api/mcp/servers` | Register MCP server |
|
||||
| GET | `/api/mcp/servers` | List MCP servers |
|
||||
| PUT | `/api/mcp/servers/{id}` | Update MCP server |
|
||||
| PUT | `/api/mcp/servers/{id}/toggle` | Activate/deactivate |
|
||||
| POST | `/api/mcp/servers/{id}/upload` | Upload script file |
|
||||
| PUT | `/api/mcp/servers/{id}/link-adb` | Link to ADB Vector |
|
||||
@@ -350,6 +353,8 @@ oci-cis-agent/
|
||||
|--------|----------|-------------|
|
||||
| POST | `/api/adb/config` | Save ADB connection (with GenAI config + embedding model) |
|
||||
| GET | `/api/adb/configs` | List ADB connections |
|
||||
| PUT | `/api/adb/configs/{id}` | Update ADB connection (multipart) |
|
||||
| POST | `/api/adb/parse-wallet` | Parse wallet ZIP and extract DSN names |
|
||||
| POST | `/api/adb/{id}/upload-wallet` | Upload wallet ZIP |
|
||||
| POST | `/api/adb/test/{id}` | Test ADB connection |
|
||||
| POST | `/api/adb/{id}/ensure-table` | Create embeddings table in ADB |
|
||||
@@ -410,6 +415,8 @@ oci-cis-agent/
|
||||
| Google | Gemini 2.5 Flash | GENERIC |
|
||||
| xAI | Grok 4 | GENERIC |
|
||||
| xAI | Grok 3 | GENERIC |
|
||||
| OpenAI | GPT-oss (120B) | GENERIC |
|
||||
| OpenAI | GPT-oss (20B) | GENERIC |
|
||||
|
||||
### Embedding Models
|
||||
|
||||
@@ -419,6 +426,7 @@ oci-cis-agent/
|
||||
| Cohere | Embed Multilingual v3.0 | 1024 |
|
||||
| Cohere | Embed English Light v3.0 | 384 |
|
||||
| Cohere | Embed Multilingual Light v3.0 | 384 |
|
||||
| Cohere | Embed v4.0 (Multimodal) | 1536 |
|
||||
|
||||
### GenAI Regions
|
||||
|
||||
@@ -446,6 +454,7 @@ oci-cis-agent/
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| **v1.4** | 2026-03 | In-place config editing (OCI, GenAI, ADB, MCP), OpenAI GPT-oss models (120B/20B), Cohere Embed v4.0 multimodal, wallet auto-parse with DSN extraction |
|
||||
| **v1.3** | 2026-03 | Persistent config logs per tab, GenAI auto-fill from OCI credentials, inline UX feedback with loading spinners, MCP type-switch fix, encrypted key passphrase support, Docker stdin hang fix |
|
||||
| **v1.2** | 2026-03 | RAG pipeline (OCI GenAI embeddings + ADB vector search), dedicated Embeddings tab, CIS report chunking, file upload embedding |
|
||||
| **v1.1** | 2025-02 | OCI SDK GenAI (exact pattern), OCI Account Explorer, MCP↔ADB linking, full chat parameters |
|
||||
|
||||
175
backend/app.py
175
backend/app.py
@@ -58,6 +58,8 @@ GENAI_MODELS = {
|
||||
"google.gemini-2.5-flash": {"provider":"google","name":"Google Gemini 2.5 Flash","api_format":"GENERIC"},
|
||||
"xai.grok-4": {"provider":"xai","name":"xAI Grok 4","api_format":"GENERIC"},
|
||||
"xai.grok-3": {"provider":"xai","name":"xAI Grok 3","api_format":"GENERIC"},
|
||||
"openai.gpt-oss-120b": {"provider":"openai","name":"OpenAI GPT-oss (120B)","api_format":"GENERIC"},
|
||||
"openai.gpt-oss-20b": {"provider":"openai","name":"OpenAI GPT-oss (20B)","api_format":"GENERIC"},
|
||||
}
|
||||
|
||||
EMBEDDING_MODELS = {
|
||||
@@ -65,6 +67,7 @@ EMBEDDING_MODELS = {
|
||||
"cohere.embed-multilingual-v3.0": {"name":"Cohere Embed Multilingual v3.0","dims":1024},
|
||||
"cohere.embed-english-light-v3.0": {"name":"Cohere Embed English Light v3.0","dims":384},
|
||||
"cohere.embed-multilingual-light-v3.0": {"name":"Cohere Embed Multilingual Light v3.0","dims":384},
|
||||
"cohere.embed-v4.0": {"name":"Cohere Embed v4.0 (Multimodal)","dims":1536},
|
||||
}
|
||||
|
||||
GENAI_REGIONS = [
|
||||
@@ -426,8 +429,9 @@ async def save_oci(
|
||||
@app.get("/api/oci/configs")
|
||||
async def list_oci(u=Depends(current_user)):
|
||||
with db() as c:
|
||||
if u["role"]=="admin": rows=c.execute("SELECT id,user_id,tenancy_name,tenancy_ocid,region,compartment_id,created_at FROM oci_configs").fetchall()
|
||||
else: rows=c.execute("SELECT id,user_id,tenancy_name,tenancy_ocid,region,compartment_id,created_at FROM oci_configs WHERE user_id=?",(u["id"],)).fetchall()
|
||||
cols = "id,user_id,tenancy_name,tenancy_ocid,user_ocid,fingerprint,region,compartment_id,created_at"
|
||||
if u["role"]=="admin": rows=c.execute(f"SELECT {cols} FROM oci_configs").fetchall()
|
||||
else: rows=c.execute(f"SELECT {cols} FROM oci_configs WHERE user_id=?",(u["id"],)).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
@app.delete("/api/oci/configs/{cid}")
|
||||
@@ -441,6 +445,43 @@ async def del_oci(cid: str, u=Depends(require("admin","user"))):
|
||||
if d.exists(): shutil.rmtree(d)
|
||||
return {"ok": True}
|
||||
|
||||
@app.put("/api/oci/configs/{cid}")
|
||||
async def update_oci(
|
||||
cid: str,
|
||||
tenancy_name: str = Form(...), tenancy_ocid: str = Form(...),
|
||||
user_ocid: str = Form(...), fingerprint: str = Form(...),
|
||||
region: str = Form(...), compartment_id: str = Form(""),
|
||||
key_passphrase: str = Form(""),
|
||||
private_key: Optional[UploadFile] = File(None), public_key: Optional[UploadFile] = File(None),
|
||||
u = Depends(require("admin","user"))
|
||||
):
|
||||
with db() as c:
|
||||
existing = c.execute("SELECT * FROM oci_configs WHERE id=?", (cid,)).fetchone()
|
||||
if not existing: raise HTTPException(404)
|
||||
if u["role"] != "admin" and existing["user_id"] != u["id"]: raise HTTPException(403)
|
||||
cdir = OCI_DIR / cid; cdir.mkdir(parents=True, exist_ok=True)
|
||||
kp = cdir / "oci_api_key.pem"
|
||||
if private_key and private_key.filename:
|
||||
key_bytes = await private_key.read()
|
||||
kp.write_bytes(key_bytes); kp.chmod(0o600)
|
||||
key_text = key_bytes.decode("utf-8", errors="ignore")
|
||||
if ("ENCRYPTED" in key_text or "Proc-Type: 4,ENCRYPTED" in key_text) and not key_passphrase:
|
||||
raise HTTPException(400, "A chave privada está criptografada (ENCRYPTED). Informe a Key Passphrase.")
|
||||
if public_key and public_key.filename:
|
||||
(cdir / "oci_api_key_public.pem").write_bytes(await public_key.read())
|
||||
cfg_file = cdir / "config"
|
||||
cfg_content = (f"[DEFAULT]\nuser={user_ocid}\nfingerprint={fingerprint}\n"
|
||||
f"tenancy={tenancy_ocid}\nregion={region}\nkey_file={kp}\n")
|
||||
if key_passphrase:
|
||||
cfg_content += f"pass_phrase={key_passphrase}\n"
|
||||
cfg_file.write_text(cfg_content); cfg_file.chmod(0o600)
|
||||
with db() as c:
|
||||
c.execute("UPDATE oci_configs SET tenancy_name=?,tenancy_ocid=?,user_ocid=?,fingerprint=?,region=?,compartment_id=? WHERE id=?",
|
||||
(tenancy_name, tenancy_ocid, user_ocid, fingerprint, region, compartment_id or None, cid))
|
||||
_audit(u["id"], u["username"], "update_oci_config", cid, f"tenancy={tenancy_name}")
|
||||
_config_log("oci", cid, tenancy_name, "success", "save", f"Credencial atualizada: {tenancy_name} ({region})", u["id"], u["username"])
|
||||
return {"id": cid, "tenancy_name": tenancy_name, "region": region}
|
||||
|
||||
@app.post("/api/oci/test/{cid}")
|
||||
async def test_oci(cid: str, u=Depends(require("admin","user"))):
|
||||
cp = OCI_DIR / cid / "config"
|
||||
@@ -595,6 +636,28 @@ async def del_genai(gid: str, u=Depends(require("admin","user"))):
|
||||
with db() as c: c.execute("DELETE FROM genai_configs WHERE id=?", (gid,))
|
||||
return {"ok": True}
|
||||
|
||||
@app.put("/api/genai/configs/{gid}")
|
||||
async def update_genai(gid: str, req: GenAIConfigReq, u=Depends(require("admin","user"))):
|
||||
with db() as c:
|
||||
existing = c.execute("SELECT * FROM genai_configs WHERE id=?", (gid,)).fetchone()
|
||||
if not existing: raise HTTPException(404)
|
||||
if u["role"] != "admin" and existing["user_id"] != u["id"]: raise HTTPException(403)
|
||||
ep = req.endpoint or f"https://inference.generativeai.{req.genai_region}.oci.oraclecloud.com"
|
||||
with db() as c:
|
||||
if req.is_default:
|
||||
c.execute("UPDATE genai_configs SET is_default=0 WHERE user_id=?", (u["id"],))
|
||||
c.execute(
|
||||
"""UPDATE genai_configs SET name=?,oci_config_id=?,model_id=?,model_ocid=?,compartment_id=?,
|
||||
genai_region=?,endpoint=?,serving_type=?,dedicated_endpoint_id=?,temperature=?,max_tokens=?,
|
||||
top_p=?,top_k=?,frequency_penalty=?,presence_penalty=?,is_default=? WHERE id=?""",
|
||||
(req.name, req.oci_config_id, req.model_id, req.model_ocid,
|
||||
req.compartment_id, req.genai_region, ep, req.serving_type, req.dedicated_endpoint_id,
|
||||
req.temperature, req.max_tokens, req.top_p, req.top_k,
|
||||
req.frequency_penalty, req.presence_penalty, int(req.is_default), gid))
|
||||
_audit(u["id"], u["username"], "update_genai_config", gid, req.model_id)
|
||||
_config_log("genai", gid, req.name, "success", "save", f"Modelo atualizado: {req.model_id} ({req.genai_region})", u["id"], u["username"])
|
||||
return {"id": gid, "model_id": req.model_id, "endpoint": ep}
|
||||
|
||||
@app.post("/api/genai/test/{gid}")
|
||||
async def test_genai(gid: str, u=Depends(require("admin","user"))):
|
||||
with db() as c:
|
||||
@@ -878,6 +941,23 @@ async def del_mcp(mid: str, u=Depends(require("admin","user"))):
|
||||
with db() as c: c.execute("DELETE FROM mcp_servers WHERE id=?", (mid,))
|
||||
return {"ok": True}
|
||||
|
||||
@app.put("/api/mcp/servers/{mid}")
|
||||
async def update_mcp(mid: str, req: MCPServerReq, u=Depends(require("admin","user"))):
|
||||
with db() as c:
|
||||
existing = c.execute("SELECT * FROM mcp_servers WHERE id=?", (mid,)).fetchone()
|
||||
if not existing: raise HTTPException(404)
|
||||
if u["role"] != "admin" and existing["user_id"] != u["id"]: raise HTTPException(403)
|
||||
with db() as c:
|
||||
c.execute(
|
||||
"UPDATE mcp_servers SET name=?,description=?,server_type=?,command=?,args=?,env_vars=?,url=?,tools=?,linked_adb_id=? WHERE id=?",
|
||||
(req.name, req.description, req.server_type, req.command,
|
||||
json.dumps(req.args) if req.args else None,
|
||||
json.dumps(req.env_vars) if req.env_vars else None, req.url,
|
||||
json.dumps(req.tools) if req.tools else None, req.linked_adb_id, mid))
|
||||
_audit(u["id"], u["username"], "update_mcp", mid, req.name)
|
||||
_config_log("mcp", mid, req.name, "success", "save", f"MCP atualizado: {req.name} ({req.server_type})", u["id"], u["username"])
|
||||
return {"id": mid, "name": req.name, "server_type": req.server_type}
|
||||
|
||||
@app.put("/api/mcp/servers/{mid}/toggle")
|
||||
async def toggle_mcp(mid: str, u=Depends(require("admin","user"))):
|
||||
with db() as c:
|
||||
@@ -900,18 +980,57 @@ async def link_mcp_adb(mid: str, adb_id: str = Query(...), u=Depends(require("ad
|
||||
return {"ok": True, "mcp_id": mid, "linked_adb_id": adb_id}
|
||||
|
||||
# ── ADB Vector DB Config ─────────────────────────────────────────────────────
|
||||
@app.post("/api/adb/parse-wallet")
|
||||
async def parse_wallet(wallet: UploadFile = File(...), u=Depends(require("admin","user"))):
|
||||
"""Extract DSN names from tnsnames.ora inside wallet ZIP (temporary parse, no save)."""
|
||||
import zipfile, tempfile
|
||||
try:
|
||||
data = await wallet.read()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
zp = Path(tmp) / "wallet.zip"
|
||||
zp.write_bytes(data)
|
||||
with zipfile.ZipFile(str(zp), 'r') as z:
|
||||
z.extractall(tmp)
|
||||
tns = Path(tmp) / "tnsnames.ora"
|
||||
if not tns.exists():
|
||||
raise HTTPException(400, "Wallet ZIP não contém tnsnames.ora")
|
||||
tns_text = tns.read_text(errors="ignore")
|
||||
dsn_names = re.findall(r'^(\w[\w\-.]*)\s*=', tns_text, re.MULTILINE)
|
||||
if not dsn_names:
|
||||
raise HTTPException(400, "Nenhum DSN encontrado no tnsnames.ora")
|
||||
return {"dsn_names": dsn_names, "files": [n for n in os.listdir(tmp) if n != "wallet.zip"]}
|
||||
except zipfile.BadZipFile:
|
||||
raise HTTPException(400, "Arquivo não é um ZIP válido")
|
||||
|
||||
@app.post("/api/adb/config")
|
||||
async def save_adb(req: ADBVectorReq, u=Depends(require("admin","user"))):
|
||||
async def save_adb(
|
||||
config_name: str = Form(...), dsn: str = Form(...), username: str = Form(...),
|
||||
password: str = Form(...), wallet_password: str = Form(""),
|
||||
table_name: str = Form("CIS_EMBEDDINGS"), use_mtls: str = Form("true"),
|
||||
genai_config_id: str = Form(""), embedding_model_id: str = Form("cohere.embed-multilingual-v3.0"),
|
||||
wallet: Optional[UploadFile] = File(None),
|
||||
u=Depends(require("admin","user"))
|
||||
):
|
||||
vid = str(uuid.uuid4())
|
||||
use_mtls_bool = use_mtls.lower() in ("true", "1", "yes")
|
||||
with db() as c:
|
||||
c.execute(
|
||||
"INSERT INTO adb_vector_configs (id,user_id,config_name,dsn,username,password_enc,wallet_password_enc,table_name,use_mtls,genai_config_id,embedding_model_id) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
|
||||
(vid, u["id"], req.config_name, req.dsn, req.username, _enc(req.password),
|
||||
_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}
|
||||
(vid, u["id"], config_name, dsn, username, _enc(password),
|
||||
_enc(wallet_password) if wallet_password else None, table_name, int(use_mtls_bool),
|
||||
genai_config_id or None, embedding_model_id))
|
||||
# Auto-save wallet if provided
|
||||
if wallet and wallet.filename:
|
||||
import zipfile
|
||||
wdir = WALLET_DIR / vid; wdir.mkdir(parents=True, exist_ok=True)
|
||||
zp = wdir / "wallet.zip"
|
||||
zp.write_bytes(await wallet.read())
|
||||
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))
|
||||
_config_log("adb", vid, config_name, "success", "upload", f"Wallet enviada com a conexão", u["id"], u["username"])
|
||||
_audit(u["id"], u["username"], "save_adb_config", vid, config_name)
|
||||
_config_log("adb", vid, config_name, "success", "save", f"Conexão salva: {config_name} ({dsn})", u["id"], u["username"])
|
||||
return {"id": vid, "config_name": config_name}
|
||||
|
||||
@app.post("/api/adb/{vid}/upload-wallet")
|
||||
async def upload_wallet(vid: str, wallet: UploadFile = File(...), u=Depends(require("admin","user"))):
|
||||
@@ -967,6 +1086,44 @@ async def del_adb(vid: str, u=Depends(require("admin","user"))):
|
||||
if d.exists(): shutil.rmtree(d)
|
||||
return {"ok": True}
|
||||
|
||||
@app.put("/api/adb/configs/{vid}")
|
||||
async def update_adb(
|
||||
vid: str,
|
||||
config_name: str = Form(...), dsn: str = Form(...), username: str = Form(...),
|
||||
password: str = Form(""), wallet_password: str = Form(""),
|
||||
table_name: str = Form("CIS_EMBEDDINGS"), use_mtls: str = Form("true"),
|
||||
genai_config_id: str = Form(""), embedding_model_id: str = Form("cohere.embed-multilingual-v3.0"),
|
||||
wallet: Optional[UploadFile] = File(None),
|
||||
u=Depends(require("admin","user"))
|
||||
):
|
||||
with db() as c:
|
||||
existing = c.execute("SELECT * FROM adb_vector_configs WHERE id=?", (vid,)).fetchone()
|
||||
if not existing: raise HTTPException(404)
|
||||
if u["role"] != "admin" and existing["user_id"] != u["id"]: raise HTTPException(403)
|
||||
use_mtls_bool = use_mtls.lower() in ("true", "1", "yes")
|
||||
sets = "config_name=?,dsn=?,username=?,table_name=?,use_mtls=?,genai_config_id=?,embedding_model_id=?"
|
||||
vals = [config_name, dsn, username, table_name, int(use_mtls_bool), genai_config_id or None, embedding_model_id]
|
||||
if password:
|
||||
sets += ",password_enc=?"
|
||||
vals.append(_enc(password))
|
||||
if wallet_password:
|
||||
sets += ",wallet_password_enc=?"
|
||||
vals.append(_enc(wallet_password))
|
||||
vals.append(vid)
|
||||
with db() as c:
|
||||
c.execute(f"UPDATE adb_vector_configs SET {sets} WHERE id=?", vals)
|
||||
if wallet and wallet.filename:
|
||||
import zipfile
|
||||
wdir = WALLET_DIR / vid; wdir.mkdir(parents=True, exist_ok=True)
|
||||
zp = wdir / "wallet.zip"
|
||||
zp.write_bytes(await wallet.read())
|
||||
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))
|
||||
_config_log("adb", vid, config_name, "success", "upload", "Wallet atualizado", u["id"], u["username"])
|
||||
_audit(u["id"], u["username"], "update_adb_config", vid, config_name)
|
||||
_config_log("adb", vid, config_name, "success", "save", f"Conexão atualizada: {config_name} ({dsn})", u["id"], u["username"])
|
||||
return {"id": vid, "config_name": config_name}
|
||||
|
||||
@app.post("/api/adb/{vid}/ensure-table")
|
||||
async def ensure_table(vid: str, u=Depends(require("admin","user"))):
|
||||
with db() as c:
|
||||
|
||||
@@ -194,7 +194,7 @@ 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:[],auditLogs:[],models:{},regions:[],embModels:{},selGenai:'',expData:null};
|
||||
const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],auditLogs:[],models:{},regions:[],embModels:{},selGenai:'',expData:null,editing:null};
|
||||
const API='/api';
|
||||
|
||||
async function $api(p,o={}){const h={...(o.headers||{})};if(S.token)h['Authorization']='Bearer '+S.token;
|
||||
@@ -212,7 +212,7 @@ async function loadData(){try{
|
||||
try{S.adbCfg=await $api('/adb/configs')}catch(e){S.adbCfg=[]}
|
||||
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 R(){document.getElementById('app').innerHTML=S.user?rApp():rLogin();bind();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();
|
||||
const tm={'oci-config':'oci','genai':'genai','adb':'adb','mcp':'mcp'};if(tm[t])setTimeout(()=>refreshCLogs(tm[t]),100)}
|
||||
|
||||
@@ -330,69 +330,81 @@ ${!S.reports.length?'<div class="emp"><div class="eic">📂</div><p>Nenhum relat
|
||||
function dlRpt(id,f){window.open(API+'/reports/'+id+'/download?fmt='+f,'_blank')}
|
||||
async function refreshDl(){S.reports=await $api('/reports');R()}
|
||||
|
||||
/* ── Edit helpers ── */
|
||||
function editCfg(type,id){S.editing={type,id};R();
|
||||
const form=document.querySelector('.cd:has(#obtn), .cd:has(#gbtn), .cd:has(#abtn), .cd:has(#mcbtn)');if(form)form.scrollIntoView({behavior:'smooth',block:'start'})}
|
||||
function cancelEdit(){S.editing=null;R()}
|
||||
|
||||
/* ── OCI Config ── */
|
||||
function rOci(){return`<div class="cd"><div class="ct">☁️ Credenciais Registradas</div><div id="ocm"></div>
|
||||
function rOci(){const eo=S.editing?.type==='oci'?S.ociCfg.find(c=>c.id===S.editing.id):null;
|
||||
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>
|
||||
${S.ociCfg.map(c=>`<tr${eo&&eo.id===c.id?' style="background:var(--bg3)"':''}><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>
|
||||
<td><button class="btn bs bsm" onclick="tOci('${c.id}')">Testar</button> <button class="btn bd bsm" onclick="dOci('${c.id}')">Excluir</button></td></tr>`).join('')}</tbody></table>
|
||||
<td><button class="btn bs bsm" onclick="editCfg('oci','${c.id}')">Editar</button> <button class="btn bs bsm" onclick="tOci('${c.id}')">Testar</button> <button class="btn bd bsm" onclick="dOci('${c.id}')">Excluir</button></td></tr>`).join('')}</tbody></table>
|
||||
${!S.ociCfg.length?'<div class="emp" style="padding:.85rem"><p>Nenhuma credencial registrada.</p></div>':''}</div>
|
||||
<div class="cd"><div class="ct">➕ Nova Credencial</div>
|
||||
<div class="cdesc">Adicione as credenciais da sua conta OCI para executar reports e acessar serviços.</div><div id="om"></div>
|
||||
<div class="cd"><div class="ct">${eo?'✏️ Editar Credencial':'➕ Nova Credencial'}</div>
|
||||
<div class="cdesc">${eo?'Editando: <strong>'+eo.tenancy_name+'</strong>':'Adicione as credenciais da sua conta OCI para executar reports e acessar serviços.'}</div><div id="om"></div>
|
||||
<div class="g2">
|
||||
<div class="ig"><label>Tenancy Name</label><input type="text" id="otn" placeholder="minha-empresa"></div>
|
||||
<div class="ig"><label>OCID User</label><input type="text" id="ouo" placeholder="ocid1.user.oc1.."></div>
|
||||
<div class="ig"><label>Fingerprint</label><input type="text" id="ofp" placeholder="aa:bb:cc:dd:..."></div>
|
||||
<div class="ig"><label>OCID Tenancy</label><input type="text" id="oto" placeholder="ocid1.tenancy.oc1.."></div>
|
||||
<div class="ig"><label>Region</label><input type="text" id="org" placeholder="sa-saopaulo-1"></div>
|
||||
<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>Tenancy Name</label><input type="text" id="otn" placeholder="minha-empresa" value="${eo?eo.tenancy_name:''}"></div>
|
||||
<div class="ig"><label>OCID User</label><input type="text" id="ouo" placeholder="ocid1.user.oc1.." value="${eo?eo.user_ocid||'':''}"></div>
|
||||
<div class="ig"><label>Fingerprint</label><input type="text" id="ofp" placeholder="aa:bb:cc:dd:..." value="${eo?eo.fingerprint||'':''}"></div>
|
||||
<div class="ig"><label>OCID Tenancy</label><input type="text" id="oto" placeholder="ocid1.tenancy.oc1.." value="${eo?eo.tenancy_ocid:''}"></div>
|
||||
<div class="ig"><label>Region</label><input type="text" id="org" placeholder="sa-saopaulo-1" value="${eo?eo.region:''}"></div>
|
||||
<div class="ig"><label>Compartment OCID</label><input type="text" id="ocp" placeholder="ocid1.compartment.oc1.." value="${eo?eo.compartment_id||'':''}"></div></div>
|
||||
<div class="g2"><div class="ig"><label>Private Key (.pem)</label>${eo?'<div class="ht">Deixe vazio para manter a chave atual</div>':''}<input type="file" id="osk" accept=".pem"></div>
|
||||
<div class="ig"><label>Public Key (.pem)</label><input type="file" id="opk" accept=".pem"></div></div>
|
||||
<div class="g2"><div class="ig"><label>Key Passphrase</label><div class="ht">Apenas se a chave privada for protegida por senha</div><input type="password" id="okp" placeholder="(opcional)"></div>
|
||||
<div class="ig"></div></div>
|
||||
<button class="btn bp" id="obtn" onclick="sOci()">Salvar Credencial</button></div>
|
||||
<div style="display:flex;gap:8px"><button class="btn bp" id="obtn" onclick="sOci()">${eo?'Salvar Alterações':'Salvar Credencial'}</button>${eo?'<button class="btn bs" onclick="cancelEdit()">Cancelar</button>':''}</div></div>
|
||||
`+rConfigLogs('oci')}
|
||||
async function sOci(){const 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);
|
||||
fd.append('key_passphrase',document.getElementById('okp').value||'');
|
||||
const sk=document.getElementById('osk').files[0];if(!sk)return sm('om','Selecione a chave privada','e');fd.append('private_key',sk);
|
||||
const sk=document.getElementById('osk').files[0];
|
||||
const isEdit=S.editing?.type==='oci';
|
||||
if(!isEdit&&!sk)return sm('om','Selecione a chave privada','e');
|
||||
if(sk)fd.append('private_key',sk);
|
||||
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'}}
|
||||
try{const url=isEdit?'/oci/configs/'+S.editing.id:'/oci/config';const method=isEdit?'PUT':'POST';
|
||||
await $api(url,{method,body:fd,headers:{}});S.editing=null;S.ociCfg=await $api('/oci/configs');sm('om','✅ Credencial '+(isEdit?'atualizada':'salva')+' com sucesso!','s');R()}catch(e){sm('om',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Salvar Credencial'}}
|
||||
async function 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 ── */
|
||||
function rGenAI(){const mOpts=Object.entries(S.models).map(([k,v])=>`<option value="${k}">${v.name} (${v.provider})</option>`).join('');
|
||||
const rOpts=S.regions.map(r=>`<option value="${r}" ${r==='us-ashburn-1'?'selected':''}>${r}</option>`).join('');
|
||||
function rGenAI(){const eg=S.editing?.type==='genai'?S.genaiCfg.find(g=>g.id===S.editing.id):null;
|
||||
const mOpts=Object.entries(S.models).map(([k,v])=>`<option value="${k}"${eg&&eg.model_id===k?' selected':''}>${v.name} (${v.provider})</option>`).join('');
|
||||
const rOpts=S.regions.map(r=>`<option value="${r}" ${eg?(r===eg.genai_region?'selected':''):(r==='us-ashburn-1'?'selected':'')}>${r}</option>`).join('');
|
||||
return`<div class="cd"><div class="ct">🧠 Modelos Configurados</div>
|
||||
<table><thead><tr><th>Config</th><th>Modelo</th><th>Região</th><th>Temp</th><th>Tokens</th><th>Ações</th></tr></thead><tbody>
|
||||
${S.genaiCfg.map(g=>{const mi=S.models[g.model_id]||{};return`<tr><td><strong>${g.name||'—'}</strong>${g.is_default?' ⭐':''}</td>
|
||||
${S.genaiCfg.map(g=>{const mi=S.models[g.model_id]||{};return`<tr${eg&&eg.id===g.id?' style="background:var(--bg3)"':''}><td><strong>${g.name||'—'}</strong>${g.is_default?' ⭐':''}</td>
|
||||
<td>${mi.name||g.model_id}<br><span class="tag">${mi.provider||'?'}</span></td>
|
||||
<td style="font-family:var(--fm);font-size:.7rem">${g.genai_region}</td>
|
||||
<td>${g.temperature}</td><td>${g.max_tokens}</td>
|
||||
<td><button class="btn bs bsm" onclick="tGenai('${g.id}')">Testar</button> <button class="btn bd bsm" onclick="dGenai('${g.id}')">Excluir</button></td></tr>`}).join('')}</tbody></table>
|
||||
<td><button class="btn bs bsm" onclick="editCfg('genai','${g.id}')">Editar</button> <button class="btn bs bsm" onclick="tGenai('${g.id}')">Testar</button> <button class="btn bd bsm" onclick="dGenai('${g.id}')">Excluir</button></td></tr>`}).join('')}</tbody></table>
|
||||
${!S.genaiCfg.length?'<div class="emp" style="padding:.85rem"><p>Nenhum modelo configurado.</p></div>':''}</div>
|
||||
<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="cd"><div class="ct">${eg?'✏️ Editar Modelo GenAI':'➕ Novo Modelo GenAI'}</div>
|
||||
<div class="cdesc">${eg?'Editando: <strong>'+eg.name+'</strong>':'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" value="${eg?eg.name:''}"></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>
|
||||
${S.ociCfg.map(c=>`<option value="${c.id}"${eg&&eg.oci_config_id===c.id?' selected':''}>${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>
|
||||
<div class="ig"><label>Model OCID (opcional)</label><div class="ht">Somente se diferente do catálogo</div><input type="text" id="gocid" placeholder="ocid1.generativeaimodel.oc1..."></div></div>
|
||||
<div class="ig"><label>Compartment OCID</label><input type="text" id="gcmp" placeholder="ocid1.compartment.oc1.." value="${eg?eg.compartment_id:''}"></div>
|
||||
<div class="ig"><label>Model OCID (opcional)</label><div class="ht">Somente se diferente do catálogo</div><input type="text" id="gocid" placeholder="ocid1.generativeaimodel.oc1..." value="${eg&&eg.model_ocid?eg.model_ocid:''}"></div></div>
|
||||
<div style="font-size:.76rem;font-weight:700;color:var(--t2);margin:.65rem 0 .4rem;letter-spacing:-.01em">Parâmetros</div>
|
||||
<div class="g3"><div class="ig"><label>Temperature</label><input type="number" id="gtemp" value="1" step="0.1" min="0" max="2"></div>
|
||||
<div class="ig"><label>Max Tokens</label><input type="number" id="gmt" value="6000" min="1" max="128000"></div>
|
||||
<div class="ig"><label>Top P</label><input type="number" id="gtp" value="0.95" step="0.05" min="0" max="1"></div></div>
|
||||
<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>
|
||||
<div class="g3"><div class="ig"><label>Temperature</label><input type="number" id="gtemp" value="${eg?eg.temperature:1}" step="0.1" min="0" max="2"></div>
|
||||
<div class="ig"><label>Max Tokens</label><input type="number" id="gmt" value="${eg?eg.max_tokens:6000}" min="1" max="128000"></div>
|
||||
<div class="ig"><label>Top P</label><input type="number" id="gtp" value="${eg?eg.top_p:0.95}" step="0.05" min="0" max="1"></div></div>
|
||||
<div class="g3"><div class="ig"><label>Top K</label><input type="number" id="gtk" value="${eg?eg.top_k:1}" min="-1" max="500"></div>
|
||||
<div class="ig"><label>Frequency Penalty</label><input type="number" id="gfp" value="${eg?eg.frequency_penalty:0}" step="0.1" min="0" max="2"></div>
|
||||
<div class="ig"><label>Presence Penalty</label><input type="number" id="gpp" value="${eg?eg.presence_penalty:0}" step="0.1" min="0" max="2"></div></div>
|
||||
<div style="display:flex;gap:8px"><button class="btn bp" id="gbtn" onclick="sGenai()">${eg?'Salvar Alterações':'Salvar Modelo'}</button>${eg?'<button class="btn bs" onclick="cancelEdit()">Cancelar</button>':''}</div></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:{
|
||||
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');
|
||||
const isEdit=S.editing?.type==='genai';const body={
|
||||
name:document.getElementById('gname').value||'default',
|
||||
oci_config_id:document.getElementById('goci').value,model_id:document.getElementById('gmod').value,
|
||||
model_ocid:document.getElementById('gocid').value||null,
|
||||
@@ -401,8 +413,9 @@ async function sGenai(){const btn=document.getElementById('gbtn');btn.disabled=t
|
||||
temperature:parseFloat(document.getElementById('gtemp').value),max_tokens:parseInt(document.getElementById('gmt').value),
|
||||
top_p:parseFloat(document.getElementById('gtp').value),top_k:parseInt(document.getElementById('gtk').value),
|
||||
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'}}
|
||||
is_default:isEdit?(S.genaiCfg.find(g=>g.id===S.editing.id)?.is_default||false):S.genaiCfg.length===0};
|
||||
try{const url=isEdit?'/genai/configs/'+S.editing.id:'/genai/config';const method=isEdit?'PUT':'POST';
|
||||
await $api(url,{method,body});S.editing=null;S.genaiCfg=await $api('/genai/configs');sm('gm','✅ Modelo '+(isEdit?'atualizado':'salvo')+' com sucesso!','s');R()}catch(e){sm('gm',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Salvar Modelo'}}
|
||||
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;
|
||||
@@ -416,15 +429,16 @@ function mcpTypeFields(){const t=document.querySelector('input[name="mt"]:checke
|
||||
if(t==='stdio'){document.getElementById('mf-cmd').style.display='';document.getElementById('mf-args').style.display=''}
|
||||
if(t==='sse')document.getElementById('mf-url').style.display='';
|
||||
if(t==='module'){document.getElementById('mf-cmd').style.display='';document.getElementById('mf-args').style.display=''}}
|
||||
function rMCP(){const adbOpts=S.adbCfg.map(a=>`<option value="${a.id}">${a.config_name}</option>`).join('');
|
||||
function rMCP(){const em=S.editing?.type==='mcp'?S.mcpSvr.find(m=>m.id===S.editing.id):null;
|
||||
const adbOpts=S.adbCfg.map(a=>`<option value="${a.id}">${a.config_name}</option>`).join('');
|
||||
return`<div class="cd"><div class="ct">🔌 Servidores Registrados</div>
|
||||
<div class="cdesc">MCP servers disponíveis para integração com ferramentas e execução de tarefas.</div>
|
||||
${S.mcpSvr.length?S.mcpSvr.map(m=>{const adb=m.linked_adb_id?S.adbCfg.find(a=>a.id===m.linked_adb_id):null;
|
||||
return`<div style="border:1px solid var(--bd);border-radius:var(--r);padding:1rem;margin-bottom:.75rem;background:var(--bg2)">
|
||||
return`<div style="border:1px solid ${em&&em.id===m.id?'var(--p)':'var(--bd)'};border-radius:var(--r);padding:1rem;margin-bottom:.75rem;background:var(--bg2)">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.65rem">
|
||||
<div><strong style="font-size:.88rem">${m.name}</strong> <span class="tag" style="margin-left:.4rem">${m.server_type}</span>
|
||||
<span class="badge ${m.is_active?'b-on':'b-off'}" style="margin-left:.35rem">${m.is_active?'Ativo':'Inativo'}</span></div>
|
||||
<div style="display:flex;gap:.35rem"><button class="btn bs bsm" onclick="tgMcp('${m.id}')">${m.is_active?'Desativar':'Ativar'}</button>
|
||||
<div style="display:flex;gap:.35rem"><button class="btn bs bsm" onclick="editCfg('mcp','${m.id}')">Editar</button> <button class="btn bs bsm" onclick="tgMcp('${m.id}')">${m.is_active?'Desativar':'Ativar'}</button>
|
||||
<button class="btn bd bsm" onclick="dMcp('${m.id}')">Excluir</button></div></div>
|
||||
${m.description?`<div style="font-size:.72rem;color:var(--t3);margin-bottom:.5rem">${m.description}</div>`:''}
|
||||
<div style="display:flex;gap:1.5rem;flex-wrap:wrap;font-size:.72rem;color:var(--t4)">
|
||||
@@ -440,34 +454,38 @@ ${m.module_path?`<div><span style="color:var(--t3);font-weight:600">Módulo:</sp
|
||||
<option value="">Sem ADB</option>${adbOpts}</select>
|
||||
<button class="btn bs bsm" id="mlb_${m.id}" onclick="lnkMcp('${m.id}')">Vincular ADB</button></div></div>`}).join('')
|
||||
:'<div class="emp" style="padding:.85rem"><p>Nenhum MCP server registrado.</p></div>'}</div>
|
||||
<div class="cd"><div class="ct">➕ Registrar Novo Server</div>
|
||||
<div class="cdesc">Configure um novo servidor MCP. Os campos se ajustam ao tipo selecionado.</div><div id="mcm"></div>
|
||||
<div class="g2"><div class="ig"><label>Nome</label><input type="text" id="mn" placeholder="CIS Benchmark Server"></div>
|
||||
<div class="ig"><label>Descrição</label><input type="text" id="md" placeholder="Executa checks CIS 3.0"></div></div>
|
||||
<div class="cd"><div class="ct">${em?'✏️ Editar Server':'➕ Registrar Novo Server'}</div>
|
||||
<div class="cdesc">${em?'Editando: <strong>'+em.name+'</strong>':'Configure um novo servidor MCP. Os campos se ajustam ao tipo selecionado.'}</div><div id="mcm"></div>
|
||||
<div class="g2"><div class="ig"><label>Nome</label><input type="text" id="mn" placeholder="CIS Benchmark Server" value="${em?em.name:''}"></div>
|
||||
<div class="ig"><label>Descrição</label><input type="text" id="md" placeholder="Executa checks CIS 3.0" value="${em&&em.description?em.description:''}"></div></div>
|
||||
<div class="ig" style="margin-bottom:.75rem"><label>Tipo de Servidor</label>
|
||||
<div style="display:flex;gap:.5rem;margin-top:.3rem">
|
||||
<label style="display:flex;align-items:center;gap:.3rem;padding:.4rem .75rem;border:1px solid var(--bd);border-radius:8px;cursor:pointer;font-size:.78rem;transition:all .2s">
|
||||
<input type="radio" name="mt" value="stdio" checked onchange="mcpTypeFields()"> <span>⌨️ stdio</span></label>
|
||||
<input type="radio" name="mt" value="stdio" ${!em||em.server_type==='stdio'?'checked':''} onchange="mcpTypeFields()"> <span>⌨️ stdio</span></label>
|
||||
<label style="display:flex;align-items:center;gap:.3rem;padding:.4rem .75rem;border:1px solid var(--bd);border-radius:8px;cursor:pointer;font-size:.78rem;transition:all .2s">
|
||||
<input type="radio" name="mt" value="sse" onchange="mcpTypeFields()"> <span>🌐 SSE (HTTP)</span></label>
|
||||
<input type="radio" name="mt" value="sse" ${em&&em.server_type==='sse'?'checked':''} onchange="mcpTypeFields()"> <span>🌐 SSE (HTTP)</span></label>
|
||||
<label style="display:flex;align-items:center;gap:.3rem;padding:.4rem .75rem;border:1px solid var(--bd);border-radius:8px;cursor:pointer;font-size:.78rem;transition:all .2s">
|
||||
<input type="radio" name="mt" value="module" onchange="mcpTypeFields()"> <span>🐍 Python Module</span></label></div></div>
|
||||
<input type="radio" name="mt" value="module" ${em&&em.server_type==='module'?'checked':''} onchange="mcpTypeFields()"> <span>🐍 Python Module</span></label></div></div>
|
||||
<div id="mf-cmd" class="mcp-f"><div class="ig"><label>Comando</label><div class="ht">Comando para iniciar o servidor</div>
|
||||
<input type="text" id="mcmd" placeholder="python3 server.py"></div></div>
|
||||
<input type="text" id="mcmd" placeholder="python3 server.py" value="${em&&em.command?em.command:''}"></div></div>
|
||||
<div id="mf-url" class="mcp-f" style="display:none"><div class="ig"><label>URL do Servidor</label><div class="ht">Endpoint SSE do servidor MCP</div>
|
||||
<input type="text" id="murl" placeholder="http://localhost:8001/sse"></div></div>
|
||||
<input type="text" id="murl" placeholder="http://localhost:8001/sse" value="${em&&em.url?em.url:''}"></div></div>
|
||||
<div id="mf-args" class="mcp-f"><div class="ig"><label>Argumentos (JSON)</label><div class="ht">Array de argumentos passados ao comando</div>
|
||||
<input type="text" id="margs" placeholder='["--config", "/path/to/config"]'></div></div>
|
||||
<input type="text" id="margs" placeholder='["--config", "/path/to/config"]' value="${em&&em.args?JSON.stringify(em.args).replace(/"/g,'"'):''}"></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>
|
||||
<select id="madb"><option value="">Nenhum</option>${S.adbCfg.map(a=>'<option value="'+a.id+'"'+(em&&em.linked_adb_id===a.id?' selected':'')+'>'+a.config_name+'</option>').join('')}</select></div>
|
||||
<div style="display:flex;gap:8px;margin-top:.75rem"><button class="btn bp" id="mcbtn" onclick="sMcp()">${em?'Salvar Alterações':'Registrar Server'}</button>${em?'<button class="btn bs" onclick="cancelEdit()">Cancelar</button>':''}</div></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){}
|
||||
async function sMcp(){const btn=document.getElementById('mcbtn');const isEdit=S.editing?.type==='mcp';
|
||||
btn.disabled=true;btn.textContent='Salvando...';sm('mcm','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> '+(isEdit?'Atualizando':'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,
|
||||
const body={name:document.getElementById('mn').value,description:document.getElementById('md').value,
|
||||
server_type:mt,command:document.getElementById('mcmd')?.value||null,url:document.getElementById('murl')?.value||null,args,
|
||||
linked_adb_id:document.getElementById('madb').value||null}});
|
||||
S.mcpSvr=await $api('/mcp/servers');sm('mcm','✅ Registrado!','s');R()}catch(e){sm('mcm',e.message,'e');btn.disabled=false;btn.textContent='Registrar Server'}}
|
||||
linked_adb_id:document.getElementById('madb').value||null};
|
||||
const url=isEdit?'/mcp/servers/'+S.editing.id:'/mcp/servers';const method=isEdit?'PUT':'POST';
|
||||
await $api(url,{method,body});S.editing=null;
|
||||
S.mcpSvr=await $api('/mcp/servers');sm('mcm','✅ '+(isEdit?'Atualizado':'Registrado')+'!','s');R()}catch(e){sm('mcm',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Registrar Server'}}
|
||||
async function tgMcp(id){await $api('/mcp/servers/'+id+'/toggle',{method:'PUT'});S.mcpSvr=await $api('/mcp/servers');R()}
|
||||
async function dMcp(id){if(!confirm('Excluir server?'))return;await $api('/mcp/servers/'+id,{method:'DELETE'});S.mcpSvr=await $api('/mcp/servers');R()}
|
||||
async function uMcp(id){const fd=new FormData();const f=document.getElementById('muf_'+id)?.files[0];if(!f)return sm('mcpm_'+id,'Selecione um arquivo .py','e');
|
||||
@@ -478,41 +496,58 @@ async function lnkMcp(id){const aid=document.getElementById('mlnk_'+id)?.value||
|
||||
try{await $api('/mcp/servers/'+id+'/link-adb?adb_id='+aid,{method:'PUT'});S.mcpSvr=await $api('/mcp/servers');sm('mcpm_'+id,'✅ '+(aid?'Vinculado ao ADB!':'ADB desvinculado!'),'s');R()}catch(e){sm('mcpm_'+id,e.message,'e');btn.disabled=false;btn.textContent='Vincular ADB'}}
|
||||
|
||||
/* ── ADB Vector ── */
|
||||
function rADB(){return`<div class="cd"><div class="ct">🗄️ Conexões Registradas</div>
|
||||
function rADB(){const ea=S.editing?.type==='adb'?S.adbCfg.find(c=>c.id===S.editing.id):null;
|
||||
return`<div class="cd"><div class="ct">🗄️ Conexões Registradas</div>
|
||||
<table><thead><tr><th>Nome</th><th>DSN</th><th>User</th><th>Tabela</th><th>mTLS</th><th>Wallet</th><th>Embed</th><th>Ações</th></tr></thead><tbody>
|
||||
${S.adbCfg.map(c=>{const em=S.embModels[c.embedding_model_id];return`<tr><td><strong>${c.config_name}</strong></td><td style="font-family:var(--fm);font-size:.68rem;color:var(--t4)">${c.dsn}</td>
|
||||
${S.adbCfg.map(c=>{const em=S.embModels[c.embedding_model_id];return`<tr${ea&&ea.id===c.id?' style="background:var(--bg3)"':''}><td><strong>${c.config_name}</strong></td><td style="font-family:var(--fm);font-size:.68rem;color:var(--t4)">${c.dsn}</td>
|
||||
<td>${c.username}</td><td><span class="tag">${c.table_name}</span></td>
|
||||
<td>${c.use_mtls?'✅':'—'}</td><td>${c.wallet_dir?'✅':'❌'}</td>
|
||||
<td>${c.genai_config_id?'<span class="tag" style="background:var(--gnl);color:var(--gn)">'+(em?em.name:c.embedding_model_id)+'</span>':'<span style="color:var(--t4)">—</span>'}</td>
|
||||
<td><button class="btn bs bsm" onclick="tAdb('${c.id}')">Testar</button> <button class="btn bs bsm" onclick="ensureTable('${c.id}')">Criar Tabela</button> <button class="btn bd bsm" onclick="dAdb('${c.id}')">Excluir</button></td></tr>`}).join('')}</tbody></table>
|
||||
<td><button class="btn bs bsm" onclick="editCfg('adb','${c.id}')">Editar</button> <button class="btn bs bsm" onclick="tAdb('${c.id}')">Testar</button> <button class="btn bs bsm" onclick="ensureTable('${c.id}')">Criar Tabela</button> <button class="btn bd bsm" onclick="dAdb('${c.id}')">Excluir</button></td></tr>`}).join('')}</tbody></table>
|
||||
${!S.adbCfg.length?'<div class="emp" style="padding:.85rem"><p>Nenhuma conexão ADB registrada.</p></div>':''}</div>
|
||||
<div class="cd"><div class="ct">➕ Nova Conexão</div>
|
||||
<div class="cdesc">Conexão via <code style="font-size:.72rem">python-oracledb</code> Thin mode com Wallet (mTLS) para Oracle Autonomous Database.</div><div id="am"></div>
|
||||
<div class="g2"><div class="ig"><label>Nome da Conexão</label><input type="text" id="an" placeholder="Produção ADB"></div>
|
||||
<div class="ig"><label>DSN (TNS Name)</label><div class="ht">Ex: myatp_high, myatp_medium</div><input type="text" id="adsn" placeholder="myatp_high"></div>
|
||||
<div class="ig"><label>Username</label><input type="text" id="auser" placeholder="ADMIN"></div>
|
||||
<div class="ig"><label>Password</label><input type="password" id="apw"></div>
|
||||
<div class="cd"><div class="ct">${ea?'✏️ Editar Conexão':'➕ Nova Conexão'}</div>
|
||||
<div class="cdesc">${ea?'Editando: <strong>'+ea.config_name+'</strong>':'Conexão via <code style="font-size:.72rem">python-oracledb</code> Thin mode com Wallet (mTLS) para Oracle Autonomous Database. Envie o Wallet ZIP para preencher o DSN automaticamente.'}</div><div id="am"></div>
|
||||
<div class="g2"><div class="ig"><label>Nome da Conexão</label><input type="text" id="an" placeholder="Produção ADB" value="${ea?ea.config_name:''}"></div>
|
||||
<div class="ig"><label>Wallet ZIP</label><div class="ht">${ea?'Envie novo wallet ou deixe vazio para manter o atual':'Envie o wallet para extrair os DSNs do tnsnames.ora'}</div>
|
||||
<div style="display:flex;gap:6px;align-items:center"><input type="file" id="awf" accept=".zip" style="flex:1"><button class="btn bs bsm" id="wpbtn" type="button" onclick="parseWallet()">Analisar</button></div></div>
|
||||
<div class="ig"><label>DSN (TNS Name)</label><div class="ht">Selecione ou digite o DSN. Ex: myatp_high</div>
|
||||
<div id="adsn-wrap"><input type="text" id="adsn" placeholder="myatp_high" value="${ea?ea.dsn:''}"></div></div>
|
||||
<div class="ig"><label>Username</label><input type="text" id="auser" placeholder="ADMIN" value="${ea?ea.username:''}"></div>
|
||||
<div class="ig"><label>Password</label><input type="password" id="apw" placeholder="${ea?'(manter atual)':''}"></div>
|
||||
<div class="ig"><label>Wallet Password</label><input type="password" id="awpw" placeholder="(opcional)"></div>
|
||||
<div class="ig"><label>Tabela de Embeddings</label><input type="text" id="atbl" value="CIS_EMBEDDINGS"></div>
|
||||
<div class="ig"><label>Tabela de Embeddings</label><input type="text" id="atbl" value="${ea?ea.table_name:'CIS_EMBEDDINGS'}"></div>
|
||||
<div class="ig"><label>Config GenAI (Embeddings)</label><div class="ht">Credenciais OCI usadas para gerar embeddings via GenAI.</div>
|
||||
<select id="agcfg"><option value="">Nenhum (sem RAG)</option>${S.genaiCfg.map(g=>'<option value="'+g.id+'">'+(g.name||g.model_id)+' · '+g.genai_region+'</option>').join('')}</select></div>
|
||||
<select id="agcfg"><option value="">Nenhum (sem RAG)</option>${S.genaiCfg.map(g=>'<option value="'+g.id+'"'+(ea&&ea.genai_config_id===g.id?' selected':'')+'>'+(g.name||g.model_id)+' · '+g.genai_region+'</option>').join('')}</select></div>
|
||||
<div class="ig"><label>Modelo de Embedding</label><div class="ht">Modelo OCI GenAI para gerar vetores.</div>
|
||||
<select id="aemb">${Object.entries(S.embModels).map(([k,v])=>'<option value="'+k+'">'+v.name+' ('+v.dims+'d)</option>').join('')}</select></div></div>
|
||||
<button class="btn bp" id="abtn" onclick="sAdb()">Salvar Conexão</button></div>
|
||||
<div class="cd"><div class="ct">📤 Upload Wallet</div><div id="wm"></div>
|
||||
<select id="aemb">${Object.entries(S.embModels).map(([k,v])=>'<option value="'+k+'"'+(ea&&ea.embedding_model_id===k?' selected':'')+'>'+v.name+' ('+v.dims+'d)</option>').join('')}</select></div></div>
|
||||
<div style="display:flex;gap:8px"><button class="btn bp" id="abtn" onclick="sAdb()">${ea?'Salvar Alterações':'Salvar Conexão'}</button>${ea?'<button class="btn bs" onclick="cancelEdit()">Cancelar</button>':''}</div></div>
|
||||
${S.adbCfg.length?`<div class="cd"><div class="ct">📤 Atualizar 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>
|
||||
<div class="ig"><label>Wallet ZIP</label><input type="file" id="awf2" accept=".zip"></div></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 parseWallet(){const f=document.getElementById('awf').files[0];if(!f)return sm('am','Selecione um arquivo wallet ZIP.','e');
|
||||
const btn=document.getElementById('wpbtn');btn.disabled=true;btn.textContent='...';
|
||||
try{const fd=new FormData();fd.append('wallet',f);const d=await $api('/adb/parse-wallet',{method:'POST',body:fd,headers:{}});
|
||||
const wrap=document.getElementById('adsn-wrap');
|
||||
wrap.innerHTML='<select id="adsn">'+d.dsn_names.map(n=>'<option value="'+n+'">'+n+'</option>').join('')+'</select>';
|
||||
sm('am','✅ Wallet analisado! '+d.dsn_names.length+' DSN(s) encontrado(s): <strong>'+d.dsn_names.join(', ')+'</strong>. Arquivos: '+d.files.join(', '),'s');
|
||||
}catch(e){sm('am',e.message,'e')}finally{btn.disabled=false;btn.textContent='Analisar'}}
|
||||
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');
|
||||
const isEdit=S.editing?.type==='adb';
|
||||
try{const fd=new FormData();fd.append('config_name',document.getElementById('an').value);
|
||||
fd.append('dsn',document.getElementById('adsn').value);fd.append('username',document.getElementById('auser').value);
|
||||
fd.append('password',document.getElementById('apw').value);fd.append('wallet_password',document.getElementById('awpw').value||'');
|
||||
fd.append('table_name',document.getElementById('atbl').value);fd.append('use_mtls','true');
|
||||
fd.append('genai_config_id',document.getElementById('agcfg').value||'');
|
||||
fd.append('embedding_model_id',document.getElementById('aemb').value||'cohere.embed-multilingual-v3.0');
|
||||
const wf=document.getElementById('awf').files[0];if(wf)fd.append('wallet',wf);
|
||||
const url=isEdit?'/adb/configs/'+S.editing.id:'/adb/config';const method=isEdit?'PUT':'POST';
|
||||
await $api(url,{method,body:fd,headers:{}});S.editing=null;
|
||||
S.adbCfg=await $api('/adb/configs');sm('am','✅ Conexão '+(isEdit?'atualizada':'salva')+'!'+(wf?' Wallet incluído.':''),'s');R()}catch(e){sm('am',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Salvar Conexão'}}
|
||||
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');
|
||||
async function uWallet(){const fd=new FormData();const f=document.getElementById('awf2').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'}}
|
||||
|
||||
Reference in New Issue
Block a user