feat: custom Oracle AI robot logo

This commit is contained in:
nogueiraguh
2026-02-27 20:20:22 -03:00
parent 3b5b0c7c2a
commit 3c1d87dde3
6 changed files with 393 additions and 170 deletions

BIN
.DS_Store vendored

Binary file not shown.

3
.idea/misc.xml generated
View File

@@ -1,4 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.10 (oci-cis-agent)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (oci-cis-agent)" project-jdk-type="Python SDK" /> <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (oci-cis-agent)" project-jdk-type="Python SDK" />
</project> </project>

View File

@@ -1,10 +1,11 @@
""" """
OCI CIS AI Agent - Backend API v2 OCI CIS AI Agent - Backend API v1.1
FastAPI with JWT auth, TOTP MFA, RBAC, OCI GenAI integration (Cohere/Meta/xAI/Google), FastAPI with JWT auth, TOTP MFA, RBAC, OCI GenAI (exact SDK pattern),
MCP Server registry, Autonomous DB vector storage, CIS reports, chat agent, audit log. OCI Account Explorer, MCP Server registry with VectorDB tool integration,
Autonomous DB vector storage, CIS reports, chat agent, audit log.
""" """
import os, json, uuid, hashlib, hmac, time, base64, struct, secrets, subprocess import os, json, uuid, hashlib, hmac, time, base64, struct, secrets, subprocess
import shutil, asyncio, sqlite3, logging, socket, re, importlib, sys import shutil, asyncio, sqlite3, logging, socket, re
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
@@ -30,6 +31,7 @@ OCI_DIR = DATA / "oci_configs"
REPORTS = DATA / "reports" REPORTS = DATA / "reports"
MCP_DIR = DATA / "mcp_servers" MCP_DIR = DATA / "mcp_servers"
WALLET_DIR = DATA / "wallets" WALLET_DIR = DATA / "wallets"
VERSION = "1.1"
for d in [DATA, OCI_DIR, REPORTS, MCP_DIR, WALLET_DIR]: for d in [DATA, OCI_DIR, REPORTS, MCP_DIR, WALLET_DIR]:
d.mkdir(parents=True, exist_ok=True) d.mkdir(parents=True, exist_ok=True)
@@ -37,7 +39,7 @@ for d in [DATA, OCI_DIR, REPORTS, MCP_DIR, WALLET_DIR]:
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
log = logging.getLogger("agent") log = logging.getLogger("agent")
app = FastAPI(title="OCI CIS AI Agent", version="2.0.0") app = FastAPI(title="OCI CIS AI Agent", version=VERSION)
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
allow_methods=["*"], allow_headers=["*"]) allow_methods=["*"], allow_headers=["*"])
security = HTTPBearer() security = HTTPBearer()
@@ -104,16 +106,21 @@ def init_db():
); );
CREATE TABLE IF NOT EXISTS genai_configs ( CREATE TABLE IF NOT EXISTS genai_configs (
id TEXT PRIMARY KEY, user_id TEXT NOT NULL, id TEXT PRIMARY KEY, user_id TEXT NOT NULL,
name TEXT NOT NULL DEFAULT 'default',
oci_config_id TEXT NOT NULL, oci_config_id TEXT NOT NULL,
model_id TEXT NOT NULL, model_id TEXT NOT NULL,
model_ocid TEXT,
compartment_id TEXT NOT NULL, compartment_id TEXT NOT NULL,
genai_region TEXT NOT NULL, genai_region TEXT NOT NULL,
endpoint TEXT NOT NULL,
serving_type TEXT DEFAULT 'ON_DEMAND', serving_type TEXT DEFAULT 'ON_DEMAND',
endpoint_id TEXT, dedicated_endpoint_id TEXT,
temperature REAL DEFAULT 0.7, temperature REAL DEFAULT 1,
max_tokens INTEGER DEFAULT 2048, max_tokens INTEGER DEFAULT 6000,
top_p REAL DEFAULT 0.9, top_p REAL DEFAULT 0.95,
top_k INTEGER DEFAULT -1, top_k INTEGER DEFAULT 1,
frequency_penalty REAL DEFAULT 0,
presence_penalty REAL DEFAULT 0,
is_default INTEGER DEFAULT 0, is_default INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')), created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (oci_config_id) REFERENCES oci_configs(id) FOREIGN KEY (oci_config_id) REFERENCES oci_configs(id)
@@ -146,6 +153,8 @@ def init_db():
server_type TEXT NOT NULL DEFAULT 'stdio', server_type TEXT NOT NULL DEFAULT 'stdio',
command TEXT, args TEXT, env_vars TEXT, command TEXT, args TEXT, env_vars TEXT,
url TEXT, module_path TEXT, url TEXT, module_path TEXT,
tools TEXT,
linked_adb_id TEXT,
is_active INTEGER DEFAULT 1, is_active INTEGER DEFAULT 1,
created_at TEXT DEFAULT (datetime('now')) created_at TEXT DEFAULT (datetime('now'))
); );
@@ -187,6 +196,12 @@ def _make_token(uid,role,sid):
def _enc(v): return base64.b64encode(v.encode()).decode() def _enc(v): return base64.b64encode(v.encode()).decode()
def _dec(v): return base64.b64decode(v.encode()).decode() def _dec(v): return base64.b64decode(v.encode()).decode()
# ── OCI SDK Client Helper ─────────────────────────────────────────────────────
def _get_oci_config(oci_config_id: str) -> dict:
import oci
config_path = str(OCI_DIR / oci_config_id / "config")
return oci.config.from_file(config_path, "DEFAULT")
# ── Auth deps ───────────────────────────────────────────────────────────────── # ── Auth deps ─────────────────────────────────────────────────────────────────
async def current_user(cred: HTTPAuthorizationCredentials = Depends(security)): async def current_user(cred: HTTPAuthorizationCredentials = Depends(security)):
try: p = pyjwt.decode(cred.credentials, APP_SECRET, algorithms=[JWT_ALG]) try: p = pyjwt.decode(cred.credentials, APP_SECRET, algorithms=[JWT_ALG])
@@ -221,13 +236,17 @@ class ChangePwReq(BaseModel):
class UserUpdateReq(BaseModel): class UserUpdateReq(BaseModel):
email: Optional[str] = None; role: Optional[str] = None; is_active: Optional[bool] = None email: Optional[str] = None; role: Optional[str] = None; is_active: Optional[bool] = None
class ChatMsg(BaseModel): class ChatMsg(BaseModel):
message: str; session_id: Optional[str] = None; model_id: Optional[str] = None; genai_config_id: Optional[str] = None message: str; session_id: Optional[str] = None; genai_config_id: Optional[str] = None
class RunReportReq(BaseModel): class RunReportReq(BaseModel):
config_id: str; mcp_server_id: Optional[str] = None; regions: Optional[List[str]] = None config_id: str; mcp_server_id: Optional[str] = None; regions: Optional[List[str]] = None
class GenAIConfigReq(BaseModel): class GenAIConfigReq(BaseModel):
oci_config_id: str; model_id: str; compartment_id: str; genai_region: str name: str = "default"
serving_type: str = "ON_DEMAND"; endpoint_id: Optional[str] = None oci_config_id: str; model_id: str; model_ocid: Optional[str] = None
temperature: float = 0.7; max_tokens: int = 2048; top_p: float = 0.9; top_k: int = -1 compartment_id: str; genai_region: str
endpoint: Optional[str] = None
serving_type: str = "ON_DEMAND"; dedicated_endpoint_id: Optional[str] = None
temperature: float = 1; max_tokens: int = 6000; top_p: float = 0.95
top_k: int = 1; frequency_penalty: float = 0; presence_penalty: float = 0
is_default: bool = False is_default: bool = False
class ADBVectorReq(BaseModel): class ADBVectorReq(BaseModel):
config_name: str; dsn: str; username: str; password: str config_name: str; dsn: str; username: str; password: str
@@ -235,7 +254,9 @@ class ADBVectorReq(BaseModel):
class MCPServerReq(BaseModel): class MCPServerReq(BaseModel):
name: str; description: Optional[str] = None; server_type: str = "stdio" name: str; description: Optional[str] = None; server_type: str = "stdio"
command: Optional[str] = None; args: Optional[List[str]] = None command: Optional[str] = None; args: Optional[List[str]] = None
env_vars: Optional[Dict[str,str]] = None; url: Optional[str] = None; module_path: Optional[str] = None env_vars: Optional[Dict[str,str]] = None; url: Optional[str] = None
module_path: Optional[str] = None; tools: Optional[List[str]] = None
linked_adb_id: Optional[str] = None
# ── Auth endpoints ──────────────────────────────────────────────────────────── # ── Auth endpoints ────────────────────────────────────────────────────────────
@app.post("/api/auth/login") @app.post("/api/auth/login")
@@ -314,9 +335,9 @@ async def update_user(uid: str, req: UserUpdateReq, adm=Depends(require("admin")
if req.role not in ("admin","user","viewer"): raise HTTPException(400, "Role inválida") if req.role not in ("admin","user","viewer"): raise HTTPException(400, "Role inválida")
sets.append("role=?"); vals.append(req.role) sets.append("role=?"); vals.append(req.role)
if req.is_active is not None: sets.append("is_active=?"); vals.append(int(req.is_active)) if req.is_active is not None: sets.append("is_active=?"); vals.append(int(req.is_active))
if sets: vals.append(uid); c_ = db() if sets:
with db() as c: vals.append(uid)
if sets: c.execute(f"UPDATE users SET {','.join(sets)} WHERE id=?", vals) with db() as c: c.execute(f"UPDATE users SET {','.join(sets)} WHERE id=?", vals)
_audit(adm["id"], adm["username"], "update_user", uid) _audit(adm["id"], adm["username"], "update_user", uid)
return {"ok": True} return {"ok": True}
@@ -340,9 +361,11 @@ async def save_oci(
kp = cdir / "oci_api_key.pem" kp = cdir / "oci_api_key.pem"
kp.write_bytes(await private_key.read()); kp.chmod(0o600) kp.write_bytes(await private_key.read()); kp.chmod(0o600)
if public_key: (cdir / "oci_api_key_public.pem").write_bytes(await public_key.read()) if public_key: (cdir / "oci_api_key_public.pem").write_bytes(await public_key.read())
(cdir / "config").write_text( cfg_file = cdir / "config"
cfg_file.write_text(
f"[DEFAULT]\nuser={user_ocid}\nfingerprint={fingerprint}\n" f"[DEFAULT]\nuser={user_ocid}\nfingerprint={fingerprint}\n"
f"tenancy={tenancy_ocid}\nregion={region}\nkey_file={kp}\n") f"tenancy={tenancy_ocid}\nregion={region}\nkey_file={kp}\n")
cfg_file.chmod(0o600)
with db() as c: with db() as c:
c.execute("INSERT INTO oci_configs (id,user_id,tenancy_name,tenancy_ocid,user_ocid,fingerprint,region,key_file_path,compartment_id) VALUES (?,?,?,?,?,?,?,?,?)", 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)) (cid, u["id"], tenancy_name, tenancy_ocid, user_ocid, fingerprint, region, str(kp), compartment_id or None))
@@ -375,11 +398,98 @@ async def test_oci(cid: str, u=Depends(require("admin","user"))):
r = subprocess.run(["oci","iam","region","list","--config-file",str(cp),"--output","json"], capture_output=True, text=True, timeout=30) 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)} if r.returncode == 0: return {"status":"success","message":"Conexão OK","data":json.loads(r.stdout)}
return {"status":"error","message":r.stderr[:500]} return {"status":"error","message":r.stderr[:500]}
except FileNotFoundError: return {"status":"error","message":"OCI CLI não instalado. Instale via admin panel."} 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 subprocess.TimeoutExpired: return {"status":"error","message":"Timeout na conexão"}
except Exception as e: return {"status":"error","message":str(e)} except Exception as e: return {"status":"error","message":str(e)}
# ── OCI GenAI Config ────────────────────────────────────────────────────────── # ── OCI Account Explorer ──────────────────────────────────────────────────────
@app.get("/api/oci/explore/{cid}/compartments")
async def explore_compartments(cid: str, u=Depends(current_user)):
try:
import oci
config = _get_oci_config(cid)
identity = oci.identity.IdentityClient(config)
with db() as c:
cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone()
root = cfg["compartment_id"] or cfg["tenancy_ocid"]
comps = identity.list_compartments(root, compartment_id_in_subtree=True).data
return [{"id":cp.id,"name":cp.name,"lifecycle_state":cp.lifecycle_state,"description":cp.description or ""} for cp in comps if cp.lifecycle_state == "ACTIVE"]
except Exception as e:
return {"error": str(e)[:500]}
@app.get("/api/oci/explore/{cid}/regions")
async def explore_regions(cid: str, u=Depends(current_user)):
try:
import oci
config = _get_oci_config(cid)
identity = oci.identity.IdentityClient(config)
with db() as c:
cfg = c.execute("SELECT tenancy_ocid FROM oci_configs WHERE id=?", (cid,)).fetchone()
regions = identity.list_region_subscriptions(cfg["tenancy_ocid"]).data
return [{"name":r.region_name,"key":r.region_key,"status":r.status,"is_home":r.is_home_region} for r in regions]
except Exception as e:
return {"error": str(e)[:500]}
@app.get("/api/oci/explore/{cid}/vcns")
async def explore_vcns(cid: str, compartment_id: str = Query(None), u=Depends(current_user)):
try:
import oci
config = _get_oci_config(cid)
vn = oci.core.VirtualNetworkClient(config)
with db() as c:
cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone()
comp = compartment_id or cfg["compartment_id"] or cfg["tenancy_ocid"]
vcns = vn.list_vcns(comp).data
return [{"id":v.id,"display_name":v.display_name,"cidr_blocks":v.cidr_blocks,"lifecycle_state":v.lifecycle_state} for v in vcns]
except Exception as e:
return {"error": str(e)[:500]}
@app.get("/api/oci/explore/{cid}/instances")
async def explore_instances(cid: str, compartment_id: str = Query(None), u=Depends(current_user)):
try:
import oci
config = _get_oci_config(cid)
compute = oci.core.ComputeClient(config)
with db() as c:
cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone()
comp = compartment_id or cfg["compartment_id"] or cfg["tenancy_ocid"]
insts = compute.list_instances(comp).data
return [{"id":i.id,"display_name":i.display_name,"shape":i.shape,"lifecycle_state":i.lifecycle_state,"region":i.region,"time_created":str(i.time_created)} for i in insts]
except Exception as e:
return {"error": str(e)[:500]}
@app.get("/api/oci/explore/{cid}/databases")
async def explore_databases(cid: str, compartment_id: str = Query(None), u=Depends(current_user)):
try:
import oci
config = _get_oci_config(cid)
db_client = oci.database.DatabaseClient(config)
with db() as c:
cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone()
comp = compartment_id or cfg["compartment_id"] or cfg["tenancy_ocid"]
adbs = db_client.list_autonomous_databases(comp).data
return [{"id":a.id,"display_name":a.display_name,"db_name":a.db_name,"lifecycle_state":a.lifecycle_state,
"cpu_core_count":a.cpu_core_count,"data_storage_size_in_tbs":a.data_storage_size_in_tbs,
"is_free_tier":a.is_free_tier} for a in adbs]
except Exception as e:
return {"error": str(e)[:500]}
@app.get("/api/oci/explore/{cid}/buckets")
async def explore_buckets(cid: str, compartment_id: str = Query(None), u=Depends(current_user)):
try:
import oci
config = _get_oci_config(cid)
os_client = oci.object_storage.ObjectStorageClient(config)
namespace = os_client.get_namespace().data
with db() as c:
cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone()
comp = compartment_id or cfg["compartment_id"] or cfg["tenancy_ocid"]
buckets = os_client.list_buckets(namespace, comp).data
return [{"name":b.name,"namespace":b.namespace,"time_created":str(b.time_created)} for b in buckets]
except Exception as e:
return {"error": str(e)[:500]}
# ── OCI GenAI Config & Chat ───────────────────────────────────────────────────
@app.get("/api/genai/models") @app.get("/api/genai/models")
async def list_genai_models(u=Depends(current_user)): async def list_genai_models(u=Depends(current_user)):
return {"models": GENAI_MODELS, "regions": GENAI_REGIONS} return {"models": GENAI_MODELS, "regions": GENAI_REGIONS}
@@ -387,15 +497,20 @@ async def list_genai_models(u=Depends(current_user)):
@app.post("/api/genai/config") @app.post("/api/genai/config")
async def save_genai(req: GenAIConfigReq, u=Depends(require("admin","user"))): async def save_genai(req: GenAIConfigReq, u=Depends(require("admin","user"))):
gid = str(uuid.uuid4()) gid = str(uuid.uuid4())
ep = req.endpoint or f"https://inference.generativeai.{req.genai_region}.oci.oraclecloud.com"
with db() as c: with db() as c:
if req.is_default: if req.is_default:
c.execute("UPDATE genai_configs SET is_default=0 WHERE user_id=?", (u["id"],)) c.execute("UPDATE genai_configs SET is_default=0 WHERE user_id=?", (u["id"],))
c.execute( c.execute(
"INSERT INTO genai_configs (id,user_id,oci_config_id,model_id,compartment_id,genai_region,serving_type,endpoint_id,temperature,max_tokens,top_p,top_k,is_default) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", """INSERT INTO genai_configs (id,user_id,name,oci_config_id,model_id,model_ocid,compartment_id,
(gid, u["id"], req.oci_config_id, req.model_id, req.compartment_id, req.genai_region, genai_region,endpoint,serving_type,dedicated_endpoint_id,temperature,max_tokens,top_p,top_k,
req.serving_type, req.endpoint_id, req.temperature, req.max_tokens, req.top_p, req.top_k, int(req.is_default))) frequency_penalty,presence_penalty,is_default) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(gid, u["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)))
_audit(u["id"], u["username"], "save_genai_config", gid, req.model_id) _audit(u["id"], u["username"], "save_genai_config", gid, req.model_id)
return {"id": gid, "model_id": req.model_id} return {"id": gid, "model_id": req.model_id, "endpoint": ep}
@app.get("/api/genai/configs") @app.get("/api/genai/configs")
async def list_genai(u=Depends(current_user)): async def list_genai(u=Depends(current_user)):
@@ -411,79 +526,123 @@ async def del_genai(gid: str, u=Depends(require("admin","user"))):
@app.post("/api/genai/test/{gid}") @app.post("/api/genai/test/{gid}")
async def test_genai(gid: str, u=Depends(require("admin","user"))): async def test_genai(gid: str, u=Depends(require("admin","user"))):
"""Test GenAI connection by sending a simple prompt"""
with db() as c: with db() as c:
gc = c.execute("SELECT g.*,o.key_file_path,o.tenancy_ocid,o.user_ocid,o.fingerprint FROM genai_configs g JOIN oci_configs o ON g.oci_config_id=o.id WHERE g.id=?",(gid,)).fetchone() gc = c.execute("SELECT * FROM genai_configs WHERE id=?",(gid,)).fetchone()
if not gc: raise HTTPException(404) if not gc: raise HTTPException(404)
try: try:
resp = await _call_genai(dict(gc), "Say 'connection successful' in one sentence.") resp = _call_genai(dict(gc), "Say 'connection successful' in one short sentence.")
return {"status":"success","message":"GenAI OK","response":resp[:200]} return {"status":"success","message":"GenAI OK","response":resp[:300]}
except Exception as e: except Exception as e:
return {"status":"error","message":str(e)[:500]} return {"status":"error","message":str(e)[:500]}
async def _call_genai(gc: dict, message: str, history: list = None) -> str: def _call_genai(gc: dict, message: str, history: list = None) -> str:
"""Call OCI Generative AI chat endpoint using OCI SDK""" """
try: Call OCI Generative AI using the exact SDK pattern from chat_demo.py.
Uses oci.generative_ai_inference with proper Message/TextContent objects.
"""
import oci import oci
except ImportError:
return "❌ OCI SDK não instalado. Execute: pip install oci"
config_path = str(Path(gc["key_file_path"]).parent / "config") # Load OCI config from stored credentials (same as ~/.oci/config)
try: config_path = str(OCI_DIR / gc["oci_config_id"] / "config")
config = oci.config.from_file(config_path, "DEFAULT") config = oci.config.from_file(config_path, "DEFAULT")
except Exception:
config = {
"user": gc["user_ocid"], "tenancy": gc["tenancy_ocid"],
"fingerprint": gc["fingerprint"], "key_file": gc["key_file_path"],
"region": gc["genai_region"]
}
endpoint = f"https://inference.generativeai.{gc['genai_region']}.oci.oraclecloud.com" # Service endpoint - built from region
client = oci.generative_ai_inference.GenerativeAiInferenceClient(config, service_endpoint=endpoint) endpoint = gc["endpoint"]
# Create inference client with retry strategy and timeout
generative_ai_inference_client = oci.generative_ai_inference.GenerativeAiInferenceClient(
config=config,
service_endpoint=endpoint,
retry_strategy=oci.retry.NoneRetryStrategy(),
timeout=(10, 240)
)
# Build ChatDetails
chat_detail = oci.generative_ai_inference.models.ChatDetails()
# Determine API format from model catalog
model_info = GENAI_MODELS.get(gc["model_id"], {}) model_info = GENAI_MODELS.get(gc["model_id"], {})
api_format = model_info.get("api_format", "GENERIC") api_format = model_info.get("api_format", "GENERIC")
if gc["serving_type"] == "DEDICATED" and gc.get("endpoint_id"):
serving_mode = oci.generative_ai_inference.models.DedicatedServingMode(endpoint_id=gc["endpoint_id"])
else:
serving_mode = oci.generative_ai_inference.models.OnDemandServingMode(model_id=gc["model_id"])
if api_format == "COHERE": if api_format == "COHERE":
chat_request = oci.generative_ai_inference.models.CohereChatRequest( # ── Cohere models (CohereChatRequest) ──
message=message, max_tokens=gc.get("max_tokens", 2048), chat_request = oci.generative_ai_inference.models.CohereChatRequest()
temperature=gc.get("temperature", 0.7), top_p=gc.get("top_p", 0.9), chat_request.api_format = oci.generative_ai_inference.models.BaseChatRequest.API_FORMAT_COHERE
api_format="COHERE" chat_request.message = message
) chat_request.max_tokens = int(gc.get("max_tokens", 6000))
chat_request.temperature = float(gc.get("temperature", 1))
chat_request.frequency_penalty = float(gc.get("frequency_penalty", 0))
chat_request.presence_penalty = float(gc.get("presence_penalty", 0))
chat_request.top_p = float(gc.get("top_p", 0.95))
chat_request.top_k = int(gc.get("top_k", 1))
if history:
chat_history = []
for h in history:
entry = oci.generative_ai_inference.models.CohereMessage()
entry.role = "USER" if h["role"] == "user" else "CHATBOT"
entry.message = h["content"]
chat_history.append(entry)
chat_request.chat_history = chat_history
else: else:
msgs = [] # ── Generic format (Meta Llama, Google, xAI) - exact chat_demo.py pattern ──
chat_request = oci.generative_ai_inference.models.GenericChatRequest()
chat_request.api_format = oci.generative_ai_inference.models.BaseChatRequest.API_FORMAT_GENERIC
chat_request.max_tokens = int(gc.get("max_tokens", 6000))
chat_request.temperature = float(gc.get("temperature", 1))
chat_request.frequency_penalty = float(gc.get("frequency_penalty", 0))
chat_request.presence_penalty = float(gc.get("presence_penalty", 0))
chat_request.top_p = float(gc.get("top_p", 0.95))
chat_request.top_k = int(gc.get("top_k", 1))
messages = []
if history: if history:
for h in history: for h in history:
role = "USER" if h["role"] == "user" else "ASSISTANT" content = oci.generative_ai_inference.models.TextContent()
msgs.append(oci.generative_ai_inference.models.GenericChatMessage( content.text = h["content"]
role=role, content=[oci.generative_ai_inference.models.TextContent(text=h["content"])] msg = oci.generative_ai_inference.models.Message()
)) msg.role = "USER" if h["role"] == "user" else "ASSISTANT"
msgs.append(oci.generative_ai_inference.models.GenericChatMessage( msg.content = [content]
role="USER", content=[oci.generative_ai_inference.models.TextContent(text=message)] messages.append(msg)
))
chat_request = oci.generative_ai_inference.models.GenericChatRequest(
messages=msgs, max_tokens=gc.get("max_tokens", 2048),
temperature=gc.get("temperature", 0.7), top_p=gc.get("top_p", 0.9),
api_format="GENERIC"
)
chat_detail = oci.generative_ai_inference.models.ChatDetails( # Current user message
compartment_id=gc["compartment_id"], serving_mode=serving_mode, chat_request=chat_request content = oci.generative_ai_inference.models.TextContent()
) content.text = message
response = client.chat(chat_detail) user_message = oci.generative_ai_inference.models.Message()
chat_response = response.data.chat_response user_message.role = "USER"
user_message.content = [content]
messages.append(user_message)
if api_format == "COHERE": chat_request.messages = messages
return chat_response.text if hasattr(chat_response, 'text') else str(chat_response)
# Serving mode - supports model_id or model_ocid
model_ref = gc.get("model_ocid") or gc["model_id"]
if gc.get("serving_type") == "DEDICATED" and gc.get("dedicated_endpoint_id"):
chat_detail.serving_mode = oci.generative_ai_inference.models.DedicatedServingMode(
endpoint_id=gc["dedicated_endpoint_id"]
)
else: else:
if hasattr(chat_response, 'choices') and chat_response.choices: chat_detail.serving_mode = oci.generative_ai_inference.models.OnDemandServingMode(
return chat_response.choices[0].message.content[0].text if chat_response.choices[0].message.content else "" model_id=model_ref
return str(chat_response) )
chat_detail.chat_request = chat_request
chat_detail.compartment_id = gc["compartment_id"]
# Execute
chat_response = generative_ai_inference_client.chat(chat_detail)
# Extract text from response
resp = chat_response.data.chat_response
if api_format == "COHERE":
return resp.text if hasattr(resp, 'text') else str(resp)
else:
if hasattr(resp, 'choices') and resp.choices:
choice = resp.choices[0]
if hasattr(choice, 'message') and choice.message and hasattr(choice.message, 'content'):
contents = choice.message.content
if contents and len(contents) > 0:
return contents[0].text
return str(resp)
# ── MCP Servers ─────────────────────────────────────────────────────────────── # ── MCP Servers ───────────────────────────────────────────────────────────────
@app.post("/api/mcp/servers") @app.post("/api/mcp/servers")
@@ -491,10 +650,11 @@ async def register_mcp(req: MCPServerReq, u=Depends(require("admin","user"))):
mid = str(uuid.uuid4()) mid = str(uuid.uuid4())
with db() as c: with db() as c:
c.execute( c.execute(
"INSERT INTO mcp_servers (id,user_id,name,description,server_type,command,args,env_vars,url,module_path) VALUES (?,?,?,?,?,?,?,?,?,?)", "INSERT INTO mcp_servers (id,user_id,name,description,server_type,command,args,env_vars,url,module_path,tools,linked_adb_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
(mid, u["id"], req.name, req.description, req.server_type, (mid, u["id"], req.name, req.description, req.server_type,
req.command, json.dumps(req.args) if req.args else None, req.command, json.dumps(req.args) if req.args else None,
json.dumps(req.env_vars) if req.env_vars else None, req.url, req.module_path)) 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) _audit(u["id"], u["username"], "register_mcp", mid, req.name)
return {"id": mid, "name": req.name, "server_type": req.server_type} return {"id": mid, "name": req.name, "server_type": req.server_type}
@@ -506,8 +666,10 @@ async def list_mcp(u=Depends(current_user)):
res = [] res = []
for r in rows: for r in rows:
d = dict(r) d = dict(r)
if d.get("args"): d["args"] = json.loads(d["args"]) for k in ("args","env_vars","tools"):
if d.get("env_vars"): d["env_vars"] = json.loads(d["env_vars"]) if d.get(k):
try: d[k] = json.loads(d[k])
except: pass
res.append(d) res.append(d)
return res return res
@@ -526,14 +688,17 @@ async def toggle_mcp(mid: str, u=Depends(require("admin","user"))):
@app.post("/api/mcp/servers/{mid}/upload") @app.post("/api/mcp/servers/{mid}/upload")
async def upload_mcp_file(mid: str, file: UploadFile = File(...), u=Depends(require("admin","user"))): async def upload_mcp_file(mid: str, file: UploadFile = File(...), u=Depends(require("admin","user"))):
"""Upload a Python MCP server script"""
sdir = MCP_DIR / mid; sdir.mkdir(parents=True, exist_ok=True) sdir = MCP_DIR / mid; sdir.mkdir(parents=True, exist_ok=True)
fp = sdir / file.filename fp = sdir / file.filename
fp.write_bytes(await file.read()) fp.write_bytes(await file.read())
with db() as c: with db() as c: c.execute("UPDATE mcp_servers SET module_path=? WHERE id=?", (str(fp), mid))
c.execute("UPDATE mcp_servers SET module_path=? WHERE id=?", (str(fp), mid))
return {"ok": True, "path": str(fp)} return {"ok": True, "path": str(fp)}
@app.put("/api/mcp/servers/{mid}/link-adb")
async def link_mcp_adb(mid: str, adb_id: str = Query(...), u=Depends(require("admin","user"))):
with db() as c: c.execute("UPDATE mcp_servers SET linked_adb_id=? WHERE id=?", (adb_id, mid))
return {"ok": True, "mcp_id": mid, "linked_adb_id": adb_id}
# ── ADB Vector DB Config ───────────────────────────────────────────────────── # ── ADB Vector DB Config ─────────────────────────────────────────────────────
@app.post("/api/adb/config") @app.post("/api/adb/config")
async def save_adb(req: ADBVectorReq, u=Depends(require("admin","user"))): async def save_adb(req: ADBVectorReq, u=Depends(require("admin","user"))):
@@ -580,7 +745,7 @@ async def test_adb(vid: str, u=Depends(require("admin","user"))):
cur = conn.cursor(); cur.execute("SELECT 1 FROM DUAL"); cur.close(); conn.close() cur = conn.cursor(); cur.execute("SELECT 1 FROM DUAL"); cur.close(); conn.close()
return {"status":"success","message":"Conexão Autonomous DB OK!"} return {"status":"success","message":"Conexão Autonomous DB OK!"}
except ImportError: except ImportError:
return {"status":"error","message":"python-oracledb não instalado. Execute: pip install oracledb"} return {"status":"error","message":"python-oracledb não disponível no container."}
except Exception as e: except Exception as e:
return {"status":"error","message":str(e)[:500]} return {"status":"error","message":str(e)[:500]}
@@ -614,7 +779,13 @@ async def _exec_report(rid, cfg, regions, mcp_server_id):
if mcp_server_id: if mcp_server_id:
with db() as c: with db() as c:
mcp = c.execute("SELECT * FROM mcp_servers WHERE id=?",(mcp_server_id,)).fetchone() mcp = c.execute("SELECT * FROM mcp_servers WHERE id=?",(mcp_server_id,)).fetchone()
if mcp and mcp["module_path"]: cmd += ["--mcp-module", mcp["module_path"]] if mcp:
if mcp["module_path"]: cmd += ["--mcp-module", mcp["module_path"]]
if mcp.get("linked_adb_id"):
adb_cfg = c.execute("SELECT * FROM adb_vector_configs WHERE id=?",(mcp["linked_adb_id"],)).fetchone()
if adb_cfg:
cmd += ["--adb-dsn", adb_cfg["dsn"], "--adb-user", adb_cfg["username"]]
if adb_cfg.get("wallet_dir"): cmd += ["--adb-wallet", adb_cfg["wallet_dir"]]
proc = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) proc = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
stdout, stderr = await proc.communicate() stdout, stderr = await proc.communicate()
jp = rdir / "report.json"; hp = rdir / "report.html" jp = rdir / "report.json"; hp = rdir / "report.html"
@@ -668,36 +839,30 @@ async def chat(msg: ChatMsg, u=Depends(current_user)):
sid = msg.session_id or str(uuid.uuid4()) sid = msg.session_id or str(uuid.uuid4())
with db() as c: with db() as c:
c.execute("INSERT INTO chat_messages (id,session_id,user_id,role,content,model_id) VALUES (?,?,?,?,?,?)", c.execute("INSERT INTO chat_messages (id,session_id,user_id,role,content,model_id) VALUES (?,?,?,?,?,?)",
(str(uuid.uuid4()), sid, u["id"], "user", msg.message, msg.model_id)) (str(uuid.uuid4()), sid, u["id"], "user", msg.message, None))
genai_cfg = None genai_cfg = None
if msg.genai_config_id: if msg.genai_config_id:
with db() as c: with db() as c:
genai_cfg = c.execute( genai_cfg = c.execute("SELECT * FROM genai_configs WHERE id=?", (msg.genai_config_id,)).fetchone()
"SELECT g.*,o.key_file_path,o.tenancy_ocid,o.user_ocid,o.fingerprint FROM genai_configs g JOIN oci_configs o ON g.oci_config_id=o.id WHERE g.id=?",
(msg.genai_config_id,)).fetchone()
elif msg.model_id:
with db() as c:
genai_cfg = c.execute(
"SELECT g.*,o.key_file_path,o.tenancy_ocid,o.user_ocid,o.fingerprint FROM genai_configs g JOIN oci_configs o ON g.oci_config_id=o.id WHERE g.user_id=? AND g.model_id=? LIMIT 1",
(u["id"], msg.model_id)).fetchone()
if genai_cfg: if genai_cfg:
try: try:
history = [] history = []
with db() as c: with db() as c:
prev = c.execute("SELECT role,content FROM chat_messages WHERE session_id=? ORDER BY created_at ASC", (sid,)).fetchall() prev = c.execute("SELECT role,content FROM chat_messages WHERE session_id=? AND role IN ('user','assistant') ORDER BY created_at ASC", (sid,)).fetchall()
history = [{"role":r["role"],"content":r["content"]} for r in prev] history = [{"role":r["role"],"content":r["content"]} for r in prev]
resp = await _call_genai(dict(genai_cfg), msg.message, history[:-1] if history else None) resp = _call_genai(dict(genai_cfg), msg.message, history[:-1] if len(history) > 1 else None)
except Exception as e: except Exception as e:
resp = f"❌ Erro GenAI: {str(e)[:300]}" resp = f"❌ Erro GenAI: {str(e)[:400]}"
else: else:
resp = _agent_respond(msg.message, u) resp = _agent_respond(msg.message, u)
mid = genai_cfg["model_id"] if genai_cfg else None
with db() as c: with db() as c:
c.execute("INSERT INTO chat_messages (id,session_id,user_id,role,content,model_id) VALUES (?,?,?,?,?,?)", c.execute("INSERT INTO chat_messages (id,session_id,user_id,role,content,model_id) VALUES (?,?,?,?,?,?)",
(str(uuid.uuid4()), sid, u["id"], "assistant", resp, msg.model_id)) (str(uuid.uuid4()), sid, u["id"], "assistant", resp, mid))
return {"session_id": sid, "response": resp, "model_id": msg.model_id} return {"session_id": sid, "response": resp, "model_id": mid}
def _agent_respond(msg, user): def _agent_respond(msg, user):
m = msg.lower().strip() m = msg.lower().strip()
@@ -709,7 +874,7 @@ def _agent_respond(msg, user):
nu=c.execute("SELECT COUNT(*) n FROM users WHERE is_active=1").fetchone()["n"] nu=c.execute("SELECT COUNT(*) n FROM users WHERE is_active=1").fetchone()["n"]
nm=c.execute("SELECT COUNT(*) n FROM mcp_servers WHERE is_active=1").fetchone()["n"] nm=c.execute("SELECT COUNT(*) n FROM mcp_servers WHERE is_active=1").fetchone()["n"]
ng=c.execute("SELECT COUNT(*) n FROM genai_configs").fetchone()["n"] ng=c.execute("SELECT COUNT(*) n FROM genai_configs").fetchone()["n"]
return (f"📊 **Status do Sistema**\n\n• OCI CLI: {cli}\n• Configs OCI: {nc}\n• GenAI Models: {ng}\n• MCP Servers: {nm}\n• Relatórios: {nr}\n• Usuários ativos: {nu}\n• Servidor: ✅ Online") return (f"📊 **Status do Sistema**\n\n• OCI CLI: {cli}\n• Configs OCI: {nc}\n• GenAI Models: {ng}\n• MCP Servers: {nm}\n• Relatórios: {nr}\n• Usuários ativos: {nu}\n• Servidor: ✅ Online\n• Versão: v{VERSION}")
if any(k in m for k in ["ajuda","help","comandos"]): if any(k in m for k in ["ajuda","help","comandos"]):
return ("🤖 **Comandos disponíveis:**\n\n• `status` — Status do sistema\n• `listar configs` — Configurações OCI\n• `verificar cis` — Checks CIS 3.0\n• `modelos` — Modelos GenAI disponíveis\n• `ajuda` — Esta mensagem\n\n💡 Configure um modelo GenAI para chat com IA.") return ("🤖 **Comandos disponíveis:**\n\n• `status` — Status do sistema\n• `listar configs` — Configurações OCI\n• `verificar cis` — Checks CIS 3.0\n• `modelos` — Modelos GenAI disponíveis\n• `ajuda` — Esta mensagem\n\n💡 Configure um modelo GenAI para chat com IA.")
if any(k in m for k in ["modelo","modelos","genai"]): if any(k in m for k in ["modelo","modelos","genai"]):
@@ -720,7 +885,7 @@ def _agent_respond(msg, user):
return "\n".join(lines) return "\n".join(lines)
if any(k in m for k in ["cis","benchmark","checks","verificar"]): if any(k in m for k in ["cis","benchmark","checks","verificar"]):
return ("🔒 **CIS OCI Foundations Benchmark 3.0**\n\n54 controles em 8 domínios:\n• IAM: 17 controles\n• Networking: 8 controles\n• Compute: 3 controles\n• Logging & Monitoring: 18 controles\n• Storage: 6 controles\n• Asset Management: 2 controles\n\nConfigure OCI e execute um relatório na aba **Report**.") return ("🔒 **CIS OCI Foundations Benchmark 3.0**\n\n54 controles em 8 domínios:\n• IAM: 17 controles\n• Networking: 8 controles\n• Compute: 3 controles\n• Logging & Monitoring: 18 controles\n• Storage: 6 controles\n• Asset Management: 2 controles\n\nConfigure OCI e execute um relatório na aba **Report**.")
return ("Sou o **OCI CIS AI Agent**. Sem modelo GenAI configurado, uso respostas locais.\n\n" return ("Sou o **OCI CIS AI Agent v" + VERSION + "**. Sem modelo GenAI configurado, uso respostas locais.\n\n"
"Para chat com IA:\n1. Configure **OCI Credentials**\n2. Configure **GenAI** com modelo e região\n3. Selecione o modelo no chat\n\nDigite **ajuda** para ver os comandos.") "Para chat com IA:\n1. Configure **OCI Credentials**\n2. Configure **GenAI** com modelo e região\n3. Selecione o modelo no chat\n\nDigite **ajuda** para ver os comandos.")
@app.delete("/api/chat/{sid}") @app.delete("/api/chat/{sid}")
@@ -758,12 +923,12 @@ async def audit_log(limit:int=Query(100,le=500), u=Depends(require("admin"))):
# ── Health ──────────────────────────────────────────────────────────────────── # ── Health ────────────────────────────────────────────────────────────────────
@app.get("/api/health") @app.get("/api/health")
async def health(): async def health():
return {"status":"ok","ts":datetime.utcnow().isoformat(),"version":"2.0.0"} return {"status":"ok","ts":datetime.utcnow().isoformat(),"version":VERSION}
@app.on_event("startup") @app.on_event("startup")
async def startup(): async def startup():
init_db() init_db()
log.info("OCI CIS AI Agent v2 API started") log.info(f"OCI CIS AI Agent v{VERSION} API started")
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn

View File

@@ -9,6 +9,7 @@ services:
- APP_SECRET=${APP_SECRET:-change-me-in-production-use-a-long-random-string} - APP_SECRET=${APP_SECRET:-change-me-in-production-use-a-long-random-string}
- JWT_EXPIRY_HOURS=${JWT_EXPIRY_HOURS:-12} - JWT_EXPIRY_HOURS=${JWT_EXPIRY_HOURS:-12}
- DATA_DIR=/data - DATA_DIR=/data
- OCI_CLI_SUPPRESS_FILE_PERMISSIONS_WARNING=True
volumes: volumes:
- agent-data:/data - agent-data:/data
ports: ports:

View File

@@ -14,7 +14,6 @@
*{margin:0;padding:0;box-sizing:border-box} *{margin:0;padding:0;box-sizing:border-box}
body{font-family:var(--fs);background:var(--bg0);color:var(--t1);min-height:100vh} body{font-family:var(--fs);background:var(--bg0);color:var(--t1);min-height:100vh}
.app{display:flex;min-height:100vh} .app{display:flex;min-height:100vh}
/* ── Sidebar ── */
.sb{width:250px;background:var(--bg1);border-right:1px solid var(--bd);display:flex;flex-direction:column;position:fixed;top:0;left:0;bottom:0;z-index:100} .sb{width:250px;background:var(--bg1);border-right:1px solid var(--bd);display:flex;flex-direction:column;position:fixed;top:0;left:0;bottom:0;z-index:100}
.sb-h{padding:1.15rem 1rem;border-bottom:1px solid var(--bd);background:linear-gradient(135deg,#c74634 0%,#a53727 100%)} .sb-h{padding:1.15rem 1rem;border-bottom:1px solid var(--bd);background:linear-gradient(135deg,#c74634 0%,#a53727 100%)}
.sb-h h1{font-size:.88rem;font-weight:700;color:#fff;font-family:var(--fm);letter-spacing:-.02em;display:flex;align-items:center;gap:.4rem} .sb-h h1{font-size:.88rem;font-weight:700;color:#fff;font-family:var(--fm);letter-spacing:-.02em;display:flex;align-items:center;gap:.4rem}
@@ -28,66 +27,59 @@ body{font-family:var(--fs);background:var(--bg0);color:var(--t1);min-height:100v
.ui{display:flex;align-items:center;gap:.5rem} .ui{display:flex;align-items:center;gap:.5rem}
.ua{width:28px;height:28px;background:var(--ac);border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.7rem;color:#fff} .ua{width:28px;height:28px;background:var(--ac);border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.7rem;color:#fff}
.un{font-weight:600;color:var(--t1);font-size:.78rem}.ur{color:var(--tm);font-size:.63rem;font-family:var(--fm)} .un{font-weight:600;color:var(--t1);font-size:.78rem}.ur{color:var(--tm);font-size:.63rem;font-family:var(--fm)}
/* ── Main ── */
.mc{flex:1;margin-left:250px;min-height:100vh} .mc{flex:1;margin-left:250px;min-height:100vh}
.tb{height:50px;background:var(--bg1);border-bottom:1px solid var(--bd);display:flex;align-items:center;justify-content:space-between;padding:0 1.15rem;position:sticky;top:0;z-index:50} .tb{height:50px;background:var(--bg1);border-bottom:1px solid var(--bd);display:flex;align-items:center;justify-content:space-between;padding:0 1.15rem;position:sticky;top:0;z-index:50}
.tb-t{font-weight:600;font-size:.88rem;color:var(--t1)}.tb-a{display:flex;align-items:center;gap:.5rem} .tb-t{font-weight:600;font-size:.88rem;color:var(--t1)}.tb-a{display:flex;align-items:center;gap:.5rem}
.pc{padding:1.15rem} .pc{padding:1.15rem}
.cd{background:var(--bg1);border:1px solid var(--bd);border-radius:var(--rl);padding:1.15rem;margin-bottom:.65rem;box-shadow:var(--sh)} .cd{background:var(--bg1);border:1px solid var(--bd);border-radius:var(--rl);padding:1.15rem;margin-bottom:.65rem;box-shadow:var(--sh)}
.ct{font-weight:600;font-size:.88rem;margin-bottom:.65rem;display:flex;align-items:center;gap:.35rem;color:var(--t1)} .ct{font-weight:600;font-size:.88rem;margin-bottom:.65rem;display:flex;align-items:center;gap:.35rem;color:var(--t1)}
/* ── Buttons ── */
.btn{display:inline-flex;align-items:center;gap:.35rem;padding:.42rem .8rem;border-radius:var(--r);font-size:.76rem;font-weight:600;cursor:pointer;border:1px solid transparent;transition:all .15s;font-family:var(--fs)} .btn{display:inline-flex;align-items:center;gap:.35rem;padding:.42rem .8rem;border-radius:var(--r);font-size:.76rem;font-weight:600;cursor:pointer;border:1px solid transparent;transition:all .15s;font-family:var(--fs)}
.bp{background:var(--ac);color:#fff;border-color:var(--ac)}.bp:hover{background:var(--ah)} .bp{background:var(--ac);color:#fff;border-color:var(--ac)}.bp:hover{background:var(--ah)}
.bs{background:var(--bg1);color:var(--t2);border-color:var(--bd)}.bs:hover{border-color:var(--tm);color:var(--t1)} .bs{background:var(--bg1);color:var(--t2);border-color:var(--bd)}.bs:hover{border-color:var(--tm);color:var(--t1)}
.bd{background:var(--rdl);color:var(--rd);border-color:rgba(199,70,52,.2)}.bd:hover{background:var(--rd);color:#fff} .bd{background:var(--rdl);color:var(--rd);border-color:rgba(199,70,52,.2)}.bd:hover{background:var(--rd);color:#fff}
.bsm{padding:.28rem .55rem;font-size:.7rem} .bsm{padding:.28rem .55rem;font-size:.7rem}
.btn:disabled{opacity:.5;cursor:not-allowed} .btn:disabled{opacity:.5;cursor:not-allowed}
/* ── Form ── */
.ig{margin-bottom:.65rem}.ig label{display:block;font-size:.76rem;font-weight:600;color:var(--t2);margin-bottom:.2rem} .ig{margin-bottom:.65rem}.ig label{display:block;font-size:.76rem;font-weight:600;color:var(--t2);margin-bottom:.2rem}
.ig .ht{font-size:.65rem;color:var(--tm);margin-bottom:.15rem} .ig .ht{font-size:.65rem;color:var(--tm);margin-bottom:.15rem}
input[type=text],input[type=password],input[type=email],input[type=number],select,textarea{width:100%;padding:.45rem .6rem;background:var(--bg1);border:1px solid var(--bd);border-radius:var(--r);color:var(--t1);font-size:.78rem;font-family:var(--fm);transition:border .15s} input[type=text],input[type=password],input[type=email],input[type=number],select,textarea{width:100%;padding:.45rem .6rem;background:var(--bg1);border:1px solid var(--bd);border-radius:var(--r);color:var(--t1);font-size:.78rem;font-family:var(--fm);transition:border .15s}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--ac);box-shadow:0 0 0 2px rgba(199,70,52,.12)} input:focus,select:focus,textarea:focus{outline:none;border-color:var(--ac);box-shadow:0 0 0 2px rgba(199,70,52,.12)}
input[type=file]{font-family:var(--fm);font-size:.73rem;color:var(--t2)} input[type=file]{font-family:var(--fm);font-size:.73rem;color:var(--t2)}
/* ── Badges ── */
.badge{padding:.15rem .5rem;border-radius:12px;font-size:.66rem;font-weight:600;font-family:var(--fm)} .badge{padding:.15rem .5rem;border-radius:12px;font-size:.66rem;font-weight:600;font-family:var(--fm)}
.b-pass{background:var(--gnl);color:var(--gn)}.b-fail{background:var(--rdl);color:var(--rd)} .b-pass{background:var(--gnl);color:var(--gn)}.b-fail{background:var(--rdl);color:var(--rd)}
.b-pend{background:var(--yll);color:var(--yl)}.b-rev{background:var(--bll);color:var(--bl)} .b-pend{background:var(--yll);color:var(--yl)}.b-rev{background:var(--bll);color:var(--bl)}
.b-admin{background:var(--ppl);color:var(--pp)}.b-user{background:var(--bll);color:var(--bl)}.b-viewer{background:var(--bg3);color:var(--tm)} .b-admin{background:var(--ppl);color:var(--pp)}.b-user{background:var(--bll);color:var(--bl)}.b-viewer{background:var(--bg3);color:var(--tm)}
.b-on{background:var(--gnl);color:var(--gn)}.b-off{background:var(--bg3);color:var(--tm)} .b-on{background:var(--gnl);color:var(--gn)}.b-off{background:var(--bg3);color:var(--tm)}
/* ── Table ── */
table{width:100%;border-collapse:collapse;font-size:.76rem} table{width:100%;border-collapse:collapse;font-size:.76rem}
th{text-align:left;padding:.5rem .6rem;font-size:.65rem;text-transform:uppercase;letter-spacing:.05em;color:var(--tm);border-bottom:2px solid var(--bd);font-weight:700;background:var(--bg2)} th{text-align:left;padding:.5rem .6rem;font-size:.65rem;text-transform:uppercase;letter-spacing:.05em;color:var(--tm);border-bottom:2px solid var(--bd);font-weight:700;background:var(--bg2)}
td{padding:.5rem .6rem;border-bottom:1px solid var(--bg3);color:var(--t2)} td{padding:.5rem .6rem;border-bottom:1px solid var(--bg3);color:var(--t2)}
tr:hover td{background:var(--acl)} tr:hover td{background:var(--acl)}
.g2{display:grid;grid-template-columns:1fr 1fr;gap:.65rem} .g2{display:grid;grid-template-columns:1fr 1fr;gap:.65rem}
.g3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:.65rem} .g3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:.65rem}
/* ── Chat ── */
.ch-c{display:flex;flex-direction:column;height:calc(100vh - 50px - 2.3rem)} .ch-c{display:flex;flex-direction:column;height:calc(100vh - 50px - 2.3rem)}
.ch-m{flex:1;overflow-y:auto;padding:.65rem;background:var(--bg2);border:1px solid var(--bd);border-radius:var(--rl) var(--rl) 0 0} .ch-m{flex:1;overflow-y:auto;padding:.65rem;background:var(--bg2);border:1px solid var(--bd);border-radius:var(--rl) var(--rl) 0 0}
.cm{margin-bottom:.65rem;display:flex;gap:.5rem} .cm{margin-bottom:.65rem;display:flex;gap:.5rem}.cm-user{justify-content:flex-end}
.cm-u{justify-content:flex-end} .cm-user .cb{background:var(--ac);color:#fff;border-radius:var(--rl) var(--rl) 4px var(--rl);border-color:var(--ac)}
.cm-u .cb{background:var(--ac);color:#fff;border-radius:var(--rl) var(--rl) 4px var(--rl);border-color:var(--ac)}
.cb{max-width:75%;padding:.55rem .75rem;border-radius:var(--rl) var(--rl) var(--rl) 4px;background:var(--bg1);font-size:.8rem;line-height:1.55;border:1px solid var(--bd);white-space:pre-wrap;box-shadow:var(--sh)} .cb{max-width:75%;padding:.55rem .75rem;border-radius:var(--rl) var(--rl) var(--rl) 4px;background:var(--bg1);font-size:.8rem;line-height:1.55;border:1px solid var(--bd);white-space:pre-wrap;box-shadow:var(--sh)}
.cb code{font-family:var(--fm);font-size:.73rem;background:var(--bg3);padding:.1rem .2rem;border-radius:3px} .cb code{font-family:var(--fm);font-size:.73rem;background:var(--bg3);padding:.1rem .2rem;border-radius:3px}
.cb strong{color:var(--ac)} .cb strong{color:var(--ac)}
.ch-i{display:flex;gap:.35rem;padding:.55rem;background:var(--bg1);border:1px solid var(--bd);border-top:none;border-radius:0 0 var(--rl) var(--rl)} .ch-i{display:flex;gap:.35rem;padding:.55rem;background:var(--bg1);border:1px solid var(--bd);border-top:none;border-radius:0 0 var(--rl) var(--rl)}
.ch-i input{flex:1} .ch-i input{flex:1}
.ch-t{display:flex;gap:.35rem;align-items:center;padding:.35rem .55rem;background:var(--bg2);border:1px solid var(--bd);border-bottom:none;border-radius:var(--rl) var(--rl) 0 0;font-size:.7rem} .ch-t{display:flex;gap:.35rem;align-items:center;padding:.35rem .55rem;background:var(--bg2);border:1px solid var(--bd);border-bottom:none;border-radius:var(--rl) var(--rl) 0 0;font-size:.7rem;flex-wrap:wrap}
.ch-t select{max-width:240px;font-size:.72rem;padding:.25rem .4rem} .ch-t select{max-width:240px;font-size:.72rem;padding:.25rem .4rem}
.ch-t label{font-weight:600;color:var(--tm);font-size:.68rem} .ch-t label{font-weight:600;color:var(--tm);font-size:.68rem}
/* ── Report iframe ── */
.rif{width:100%;height:calc(100vh - 50px - 5.5rem);border:1px solid var(--bd);border-radius:var(--rl);background:#fff} .rif{width:100%;height:calc(100vh - 50px - 5.5rem);border:1px solid var(--bd);border-radius:var(--rl);background:#fff}
/* ── Login ── */
.lp{min-height:100vh;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#fef2f0 0%,#f0f2f5 50%,#e6f1fa 100%)} .lp{min-height:100vh;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#fef2f0 0%,#f0f2f5 50%,#e6f1fa 100%)}
.lc{background:var(--bg1);border:1px solid var(--bd);border-radius:var(--rl);padding:2rem;width:100%;max-width:370px;box-shadow:0 4px 24px rgba(0,0,0,.08)} .lc{background:var(--bg1);border:1px solid var(--bd);border-radius:var(--rl);padding:2rem;width:100%;max-width:370px;box-shadow:0 4px 24px rgba(0,0,0,.08)}
.lc h2{font-size:1.1rem;font-family:var(--fm);color:var(--ac)}.lc .st{color:var(--tm);font-size:.76rem;margin-bottom:1.15rem} .lc h2{font-size:1.1rem;font-family:var(--fm);color:var(--ac)}.lc .st{color:var(--tm);font-size:.76rem;margin-bottom:1.15rem}
/* ── Alerts ── */
.al{padding:.55rem .75rem;border-radius:var(--r);font-size:.76rem;margin-bottom:.65rem} .al{padding:.55rem .75rem;border-radius:var(--r);font-size:.76rem;margin-bottom:.65rem}
.al-e{background:var(--rdl);color:var(--rd);border:1px solid rgba(199,70,52,.2)} .al-e{background:var(--rdl);color:var(--rd);border:1px solid rgba(199,70,52,.2)}
.al-s{background:var(--gnl);color:var(--gn);border:1px solid rgba(13,138,65,.2)} .al-s{background:var(--gnl);color:var(--gn);border:1px solid rgba(13,138,65,.2)}
.al-i{background:var(--bll);color:var(--bl);border:1px solid rgba(11,114,185,.2)} .al-i{background:var(--bll);color:var(--bl);border:1px solid rgba(11,114,185,.2)}
.tag{display:inline-block;padding:.1rem .35rem;background:var(--acl);color:var(--ac);border-radius:4px;font-size:.65rem;font-family:var(--fm);font-weight:500} .tag{display:inline-block;padding:.1rem .35rem;background:var(--acl);color:var(--ac);border-radius:4px;font-size:.65rem;font-family:var(--fm);font-weight:500}
.emp{text-align:center;padding:2rem;color:var(--tm)}.emp .eic{font-size:2rem;margin-bottom:.4rem} .emp{text-align:center;padding:2rem;color:var(--tm)}.emp .eic{font-size:2rem;margin-bottom:.4rem}
.exp-r{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:.5rem;margin-top:.5rem}
.exp-c{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);padding:.65rem;font-size:.76rem}
.exp-c strong{color:var(--t1);font-size:.8rem}.exp-c .em{font-family:var(--fm);font-size:.63rem;color:var(--tm);word-break:break-all}
@keyframes fi{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.fi{animation:fi .2s ease} @keyframes fi{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.fi{animation:fi .2s ease}
@keyframes pu{0%,100%{opacity:1}50%{opacity:.5}}.ld{animation:pu 1.5s infinite;color:var(--tm);font-size:.8rem;text-align:center;padding:1.2rem} @keyframes pu{0%,100%{opacity:1}50%{opacity:.5}}.ld{animation:pu 1.5s infinite;color:var(--tm);font-size:.8rem;text-align:center;padding:1.2rem}
::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:var(--bg0)}::-webkit-scrollbar-thumb{background:var(--bd);border-radius:3px} ::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:var(--bg0)}::-webkit-scrollbar-thumb{background:var(--bd);border-radius:3px}
@@ -97,7 +89,8 @@ tr:hover td{background:var(--acl)}
<body> <body>
<div id="app"></div> <div id="app"></div>
<script> <script>
const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],auditLogs:[],models:{},regions:[],selModel:'',selGenai:''}; const V='1.1';
const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],auditLogs:[],models:{},regions:[],selGenai:'',expData:null};
const API='/api'; const API='/api';
async function $api(p,o={}){const h={...(o.headers||{})};if(S.token)h['Authorization']='Bearer '+S.token; async function $api(p,o={}){const h={...(o.headers||{})};if(S.token)h['Authorization']='Bearer '+S.token;
if(o.body&&!(o.body instanceof FormData)){h['Content-Type']='application/json';o.body=JSON.stringify(o.body)} if(o.body&&!(o.body instanceof FormData)){h['Content-Type']='application/json';o.body=JSON.stringify(o.body)}
@@ -112,14 +105,14 @@ async function loadData(){try{
[S.ociCfg,S.reports,S.genaiCfg,S.mcpSvr]=await Promise.all([$api('/oci/configs'),$api('/reports'),$api('/genai/configs'),$api('/mcp/servers')]); [S.ociCfg,S.reports,S.genaiCfg,S.mcpSvr]=await Promise.all([$api('/oci/configs'),$api('/reports'),$api('/genai/configs'),$api('/mcp/servers')]);
try{const m=await $api('/genai/models');S.models=m.models;S.regions=m.regions}catch(e){} try{const m=await $api('/genai/models');S.models=m.models;S.regions=m.regions}catch(e){}
try{S.adbCfg=await $api('/adb/configs')}catch(e){S.adbCfg=[]} try{S.adbCfg=await $api('/adb/configs')}catch(e){S.adbCfg=[]}
if(S.user.role==='admin'){S.users=await $api('/users')} if(S.user.role==='admin'){try{S.users=await $api('/users')}catch(e){}}
}catch(e){console.error(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()}
function switchTab(t){S.tab=t;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()}
function rLogin(){return`<div class="lp"><div class="lc fi"> function rLogin(){return`<div class="lp"><div class="lc fi">
<div style="text-align:center;margin-bottom:1.15rem"><div style="font-size:1.6rem;margin-bottom:.3rem">🛡️</div> <div style="text-align:center;margin-bottom:1.15rem"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="48" height="48"><rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(199,70,52,0.1)" stroke="#C74634" stroke-width="2"/><rect x="11" y="11" width="14" height="14" rx="4" fill="#fff" stroke="#C74634" stroke-width="0.5"/><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.3" stroke="#C74634" stroke-width="0.8"/><line x1="11" y1="18" x2="6" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.5"/><line x1="25" y1="18" x2="30" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.5"/><circle cx="5.5" cy="18" r="1" fill="#C74634" opacity="0.4"/><circle cx="30.5" cy="18" r="1" fill="#C74634" opacity="0.4"/></svg>
<h2>OCI CIS Agent</h2><div class="st">Oracle Cloud Infrastructure · Security</div></div> <h2>OCI CIS Agent</h2><div class="st">Oracle Cloud Infrastructure · Security · v${V}</div></div>
<div id="le"></div> <div id="le"></div>
<div id="mfa-s" style="display:none"><div class="al al-i">MFA obrigatório. Insira o código do autenticador.</div> <div id="mfa-s" style="display:none"><div class="al al-i">MFA obrigatório. Insira o código do autenticador.</div>
<div class="ig"><label>Código MFA</label><input type="text" id="mfa-c" placeholder="000000" maxlength="6" style="text-align:center;font-size:1rem;letter-spacing:.25em"></div> <div class="ig"><label>Código MFA</label><input type="text" id="mfa-c" placeholder="000000" maxlength="6" style="text-align:center;font-size:1rem;letter-spacing:.25em"></div>
@@ -131,11 +124,11 @@ function rLogin(){return`<div class="lp"><div class="lc fi">
function rApp(){return`<div class="app">${rSb()}<div class="mc">${rTb()}<div class="pc fi" id="pg">${rPg()}</div></div></div>`} function rApp(){return`<div class="app">${rSb()}<div class="mc">${rTb()}<div class="pc fi" id="pg">${rPg()}</div></div></div>`}
function rSb(){ function rSb(){
const tabs=[['chat','💬','Chat Agent'],['report','📊','Report'],['downloads','📁','Downloads']]; const tabs=[['chat','💬','Chat Agent'],['explorer','🔍','OCI Explorer'],['report','📊','Report'],['downloads','📁','Downloads']];
const ctabs=[['oci-config','☁️','OCI Credentials'],['genai','🧠','GenAI Config'],['mcp','🔌','MCP Servers'],['adb','🗄️','ADB Vector']]; const ctabs=[['oci-config','☁️','OCI Credentials'],['genai','🧠','GenAI Config'],['mcp','🔌','MCP Servers'],['adb','🗄️','ADB Vector']];
const atabs=[['users','👥','Usuários'],['mfa','🔐','MFA'],['audit','📋','Audit Log']]; const atabs=[['users','👥','Usuários'],['mfa','🔐','MFA'],['audit','📋','Audit Log']];
const i=(S.user?.username||'?')[0].toUpperCase(); const i=(S.user?.username||'?')[0].toUpperCase();
return`<div class="sb"><div class="sb-h"><h1>🛡️ OCI CIS Agent</h1><div class="st">Infrastructure & Security Engineer</div></div> return`<div class="sb"><div class="sb-h"><h1><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="28" height="28" style="vertical-align:middle;flex-shrink:0"><rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(255,255,255,0.15)" 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> OCI CIS Agent</h1><div class="st">Infrastructure & Security · v${V}</div></div>
<div class="nav"><div class="nl">Principal</div> <div class="nav"><div class="nl">Principal</div>
${tabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[0]}')"><span class="ic">${t[1]}</span>${t[2]}</div>`).join('')} ${tabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[0]}')"><span class="ic">${t[1]}</span>${t[2]}</div>`).join('')}
<div class="nl" style="margin-top:.5rem">Configuração</div> <div class="nl" style="margin-top:.5rem">Configuração</div>
@@ -145,16 +138,16 @@ ${atabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[
<div class="sb-f"><div class="ui"><div class="ua">${i}</div><div><div class="un">${S.user?.username}</div><div class="ur">${S.user?.role}</div></div> <div class="sb-f"><div class="ui"><div class="ua">${i}</div><div><div class="un">${S.user?.username}</div><div class="ur">${S.user?.role}</div></div>
<button class="btn bs bsm" style="margin-left:auto" onclick="doLogout()" title="Sair">⏻</button></div></div></div>`} <button class="btn bs bsm" style="margin-left:auto" onclick="doLogout()" title="Sair">⏻</button></div></div></div>`}
function rTb(){const t={'chat':'💬 AI Agent Chat','report':'📊 Compliance Report','downloads':'📁 Downloads','oci-config':'☁️ OCI Credentials','genai':'🧠 OCI Generative AI','mcp':'🔌 MCP Servers','adb':'🗄️ Autonomous DB Vector','users':'👥 Usuários','mfa':'🔐 MFA','audit':'📋 Audit Log'}; function rTb(){const t={'chat':'💬 AI Agent Chat','explorer':'🔍 OCI Account Explorer','report':'📊 Compliance Report','downloads':'📁 Downloads','oci-config':'☁️ OCI Credentials','genai':'🧠 OCI Generative AI','mcp':'🔌 MCP Servers','adb':'🗄️ Autonomous DB Vector','users':'👥 Usuários','mfa':'🔐 MFA','audit':'📋 Audit Log'};
return`<div class="tb"><div class="tb-t">${t[S.tab]||''}</div><div class="tb-a"><span class="tag">v2.0</span></div></div>`} return`<div class="tb"><div class="tb-t">${t[S.tab]||''}</div><div class="tb-a"><span class="tag">v${V}</span></div></div>`}
function rPg(){switch(S.tab){case'chat':return rChat();case'report':return rReport();case'downloads':return rDl();case'oci-config':return rOci();case'genai':return rGenAI();case'mcp':return rMCP();case'adb':return rADB();case'users':return rUsers();case'mfa':return rMfa();case'audit':return rAudit();default:return''}} function rPg(){switch(S.tab){case'chat':return rChat();case'explorer':return rExplorer();case'report':return rReport();case'downloads':return rDl();case'oci-config':return rOci();case'genai':return rGenAI();case'mcp':return rMCP();case'adb':return rADB();case'users':return rUsers();case'mfa':return rMfa();case'audit':return rAudit();default:return''}}
// ── Chat ── // ── Chat ──
function rChat(){ function rChat(){
const ms=S.msgs.length===0?'<div class="emp"><div class="eic">🤖</div><p>Envie uma mensagem para iniciar.</p><p style="font-size:.72rem;margin-top:.3rem">Selecione um modelo GenAI acima ou use o agente local.</p></div>' const ms=S.msgs.length===0?'<div class="emp"><div class="eic">🤖</div><p>Envie uma mensagem para iniciar.</p><p style="font-size:.72rem;margin-top:.3rem">Selecione um modelo GenAI acima ou use o agente local.</p></div>'
:S.msgs.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}</div></div>`).join(''); :S.msgs.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}</div></div>`).join('');
const gcOpts=S.genaiCfg.map(g=>{const mi=S.models[g.model_id];return`<option value="${g.id}" ${S.selGenai===g.id?'selected':''}>${mi?mi.name:g.model_id} (${g.genai_region})</option>`}).join(''); const gcOpts=S.genaiCfg.map(g=>{const mi=S.models[g.model_id];return`<option value="${g.id}" ${S.selGenai===g.id?'selected':''}>${g.name||mi?.name||g.model_id} (${g.genai_region})</option>`}).join('');
return`<div class="ch-c"> return`<div class="ch-c">
<div class="ch-t"><label>🧠 Modelo:</label><select id="gsel" onchange="S.selGenai=this.value"><option value="">Agente Local (sem IA)</option>${gcOpts}</select> <div class="ch-t"><label>🧠 Modelo:</label><select id="gsel" onchange="S.selGenai=this.value"><option value="">Agente Local (sem IA)</option>${gcOpts}</select>
<div style="flex:1"></div><button class="btn bd bsm" onclick="clrChat()">Limpar</button></div> <div style="flex:1"></div><button class="btn bd bsm" onclick="clrChat()">Limpar</button></div>
@@ -170,20 +163,40 @@ async function sChat(){const el=document.getElementById('chi');const m=el.value.
function scCh(){setTimeout(()=>{const e=document.getElementById('chm');if(e)e.scrollTop=e.scrollHeight},50)} function scCh(){setTimeout(()=>{const e=document.getElementById('chm');if(e)e.scrollTop=e.scrollHeight},50)}
async function clrChat(){if(S.sid)try{await $api('/chat/'+S.sid,{method:'DELETE'})}catch(e){}S.msgs=[];S.sid=null;R()} async function clrChat(){if(S.sid)try{await $api('/chat/'+S.sid,{method:'DELETE'})}catch(e){}S.msgs=[];S.sid=null;R()}
// ── OCI Explorer ──
function rExplorer(){
const cfgSel=S.ociCfg.map(c=>`<option value="${c.id}">${c.tenancy_name} (${c.region})</option>`).join('');
if(!S.ociCfg.length)return`<div class="cd"><div class="emp"><div class="eic">☁️</div><p>Nenhuma credencial OCI configurada.</p><p style="font-size:.72rem;margin-top:.3rem">Vá em <strong>OCI Credentials</strong> para adicionar.</p></div></div>`;
return`<div class="cd"><div class="ct">🔍 Explorar Conta OCI</div>
<div class="g3"><div class="ig"><label>Conexão OCI</label><select id="exCfg">${cfgSel}</select></div>
<div class="ig"><label>Recurso</label><select id="exRes"><option value="compartments">Compartments</option><option value="regions">Regions</option><option value="instances">Compute Instances</option><option value="vcns">VCNs</option><option value="databases">Autonomous DBs</option><option value="buckets">Object Storage Buckets</option></select></div>
<div class="ig"><label>&nbsp;</label><button class="btn bp" onclick="doExplore()">🔍 Explorar</button></div></div></div>
<div class="cd"><div class="ct" id="exTitle">Resultados</div><div id="exOut">${S.expData?renderExpData(S.expData):'<div class="emp"><p>Selecione uma conexão e recurso acima.</p></div>'}</div></div>`}
async function doExplore(){const cid=document.getElementById('exCfg').value;const res=document.getElementById('exRes').value;
document.getElementById('exOut').innerHTML='<div class="ld">Consultando OCI...</div>';
try{const d=await $api('/oci/explore/'+cid+'/'+res);S.expData=d;
document.getElementById('exTitle').textContent='Resultados: '+res;
document.getElementById('exOut').innerHTML=renderExpData(d)}catch(e){document.getElementById('exOut').innerHTML='<div class="al al-e">'+e.message+'</div>'}}
function renderExpData(d){if(d.error)return`<div class="al al-e">${d.error}</div>`;if(!Array.isArray(d)||!d.length)return'<div class="emp"><p>Nenhum resultado.</p></div>';
return`<div style="font-size:.72rem;color:var(--tm);margin-bottom:.4rem">${d.length} item(ns)</div><div class="exp-r">${d.map(i=>{
const keys=Object.keys(i).filter(k=>k!=='id');
const name=i.display_name||i.name||i.db_name||'—';
return`<div class="exp-c"><strong>${name}</strong>${i.lifecycle_state?` <span class="badge ${i.lifecycle_state==='RUNNING'||i.lifecycle_state==='ACTIVE'||i.lifecycle_state==='AVAILABLE'?'b-pass':'b-pend'}">${i.lifecycle_state}</span>`:''}<br>
${keys.filter(k=>!['display_name','name','db_name','lifecycle_state'].includes(k)).map(k=>`<span style="color:var(--tm);font-size:.65rem">${k}:</span> <span class="em">${typeof i[k]==='object'?JSON.stringify(i[k]):i[k]}</span>`).join('<br>')}</div>`}).join('')}</div>`}
// ── Report ── // ── Report ──
function rReport(){const comp=S.reports.filter(r=>r.status==='completed'); function rReport(){const comp=S.reports.filter(r=>r.status==='completed');
if(!comp.length)return`<div class="cd"><div class="ct">📊 Report</div><div class="emp"><div class="eic">📄</div><p>Nenhum relatório.</p></div></div>${rRunRpt()}`; if(!comp.length)return`<div class="cd"><div class="ct">📊 Report</div><div class="emp"><div class="eic">📄</div><p>Nenhum relatório.</p></div></div>${rRunRpt()}`;
const lt=comp[0]; const lt=comp[0];
return`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem"><span style="font-size:.76rem;color:var(--tm)">Report HTML — ${lt.tenancy_name}</span> return`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem"><span style="font-size:.76rem;color:var(--tm)">Report HTML — ${lt.tenancy_name}</span>
<div style="display:flex;gap:.35rem"><select id="rsel" onchange="ldRpt(this.value)" style="max-width:260px"> <select id="rsel" onchange="ldRpt(this.value)" style="max-width:260px">${comp.map(r=>`<option value="${r.id}">${r.tenancy_name}${r.created_at}</option>`).join('')}</select></div>
${comp.map(r=>`<option value="${r.id}">${r.tenancy_name}${r.created_at}</option>`).join('')}</select></div></div>
<iframe class="rif" id="rfr" src="${API}/reports/${lt.id}/html"></iframe>${rRunRpt()}`} <iframe class="rif" id="rfr" src="${API}/reports/${lt.id}/html"></iframe>${rRunRpt()}`}
function rRunRpt(){if(S.user?.role==='viewer')return''; function rRunRpt(){if(S.user?.role==='viewer')return'';
return`<div class="cd" style="margin-top:.65rem"><div class="ct">🚀 Executar Relatório</div> return`<div class="cd" style="margin-top:.65rem"><div class="ct">🚀 Executar Relatório</div>
<div class="g3"><div class="ig"><label>Config OCI</label><select id="rc"><option value="">Selecione...</option> <div class="g3"><div class="ig"><label>Config OCI</label><select id="rc"><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}">${c.tenancy_name} (${c.region})</option>`).join('')}</select></div>
<div class="ig"><label>MCP Server (opcional)</label><select id="rmcp"><option value="">Padrão (stub)</option> <div class="ig"><label>MCP Server (opcional)</label><select id="rmcp"><option value="">Padrão (stub)</option>
${S.mcpSvr.filter(m=>m.is_active).map(m=>`<option value="${m.id}">${m.name}</option>`).join('')}</select></div> ${S.mcpSvr.filter(m=>m.is_active).map(m=>`<option value="${m.id}">${m.name}${m.linked_adb_id?' 🗄️':''}</option>`).join('')}</select></div>
<div class="ig"><label>Regiões (opcional)</label><input type="text" id="rr" placeholder="sa-saopaulo-1, us-ashburn-1"></div></div> <div class="ig"><label>Regiões (opcional)</label><input type="text" id="rr" placeholder="sa-saopaulo-1, us-ashburn-1"></div></div>
<button class="btn bp" onclick="runRpt()">▶ Executar CIS Compliance</button></div>`} <button class="btn bp" onclick="runRpt()">▶ Executar CIS Compliance</button></div>`}
function ldRpt(id){const f=document.getElementById('rfr');if(f)f.src=API+'/reports/'+id+'/html'} function ldRpt(id){const f=document.getElementById('rfr');if(f)f.src=API+'/reports/'+id+'/html'}
@@ -196,17 +209,15 @@ async function runRpt(){const c=document.getElementById('rc').value;if(!c)return
// ── Downloads ── // ── Downloads ──
function rDl(){return`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem"> function rDl(){return`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem">
<span style="font-size:.76rem;color:var(--tm)">${S.reports.length} relatório(s)</span> <span style="font-size:.76rem;color:var(--tm)">${S.reports.length} relatório(s)</span>
<div style="display:flex;gap:.35rem"><input type="text" id="dlf" placeholder="Filtrar..." style="max-width:240px" oninput="fDl()"> <button class="btn bp bsm" onclick="refreshDl()">🔄</button></div>
<button class="btn bp bsm" onclick="refreshDl()">🔄</button></div></div>
<div class="cd"><table><thead><tr><th>Tenancy</th><th>Status</th><th>Criado</th><th>Concluído</th><th>Ações</th></tr></thead> <div class="cd"><table><thead><tr><th>Tenancy</th><th>Status</th><th>Criado</th><th>Concluído</th><th>Ações</th></tr></thead>
<tbody id="dlt">${S.reports.map(r=>`<tr data-n="${r.tenancy_name.toLowerCase()}"><td><strong>${r.tenancy_name}</strong></td> <tbody>${S.reports.map(r=>`<tr><td><strong>${r.tenancy_name}</strong></td>
<td><span class="badge ${r.status==='completed'?'b-pass':r.status==='failed'?'b-fail':'b-pend'}">${r.status}</span></td> <td><span class="badge ${r.status==='completed'?'b-pass':r.status==='failed'?'b-fail':'b-pend'}">${r.status}</span></td>
<td>${r.created_at||'—'}</td><td>${r.completed_at||'—'}</td> <td>${r.created_at||'—'}</td><td>${r.completed_at||'—'}</td>
<td>${r.status==='completed'?`<button class="btn bs bsm" onclick="dlRpt('${r.id}','json')">JSON</button> <button class="btn bs bsm" onclick="dlRpt('${r.id}','html')">HTML</button>`:'—'}</td></tr>`).join('')}</tbody></table> <td>${r.status==='completed'?`<button class="btn bs bsm" onclick="dlRpt('${r.id}','json')">JSON</button> <button class="btn bs bsm" onclick="dlRpt('${r.id}','html')">HTML</button>`:'—'}</td></tr>`).join('')}</tbody></table>
${!S.reports.length?'<div class="emp"><div class="eic">📂</div><p>Nenhum relatório.</p></div>':''}</div>`} ${!S.reports.length?'<div class="emp"><div class="eic">📂</div><p>Nenhum relatório.</p></div>':''}</div>`}
function dlRpt(id,f){window.open(API+'/reports/'+id+'/download?fmt='+f,'_blank')} function dlRpt(id,f){window.open(API+'/reports/'+id+'/download?fmt='+f,'_blank')}
async function refreshDl(){S.reports=await $api('/reports');R()} async function refreshDl(){S.reports=await $api('/reports');R()}
function fDl(){const q=document.getElementById('dlf').value.toLowerCase();document.querySelectorAll('#dlt tr').forEach(tr=>{tr.style.display=tr.dataset.n?.includes(q)?'':'none'})}
// ── OCI Config ── // ── OCI Config ──
function rOci(){return`<div class="cd"><div class="ct">☁️ Credenciais OCI</div> function rOci(){return`<div class="cd"><div class="ct">☁️ Credenciais OCI</div>
@@ -234,69 +245,92 @@ async function sOci(){const fd=new FormData();fd.append('tenancy_name',document.
try{await $api('/oci/config',{method:'POST',body:fd,headers:{}});S.ociCfg=await $api('/oci/configs');sm('om','✅ Salvo!','s');R()}catch(e){sm('om',e.message,'e')}} try{await $api('/oci/config',{method:'POST',body:fd,headers:{}});S.ociCfg=await $api('/oci/configs');sm('om','✅ Salvo!','s');R()}catch(e){sm('om',e.message,'e')}}
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'});alert(d.status==='success'?'✅ Conexão OK!':'❌ '+d.message)}catch(e){alert(e.message)}}
async function dOci(id){if(!confirm('Excluir?'))return;await $api('/oci/configs/'+id,{method:'DELETE'});S.ociCfg=await $api('/oci/configs');R()} async function dOci(id){if(!confirm('Excluir?'))return;await $api('/oci/configs/'+id,{method:'DELETE'});S.ociCfg=await $api('/oci/configs');R()}
// ── GenAI Config ── // ── GenAI Config ──
function rGenAI(){const mOpts=Object.entries(S.models).map(([k,v])=>`<option value="${k}">${v.name} (${v.provider})</option>`).join(''); 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}</option>`).join(''); const rOpts=S.regions.map(r=>`<option value="${r}" ${r==='us-ashburn-1'?'selected':''}>${r}</option>`).join('');
return`<div class="cd"><div class="ct">🧠 Configurações GenAI</div> return`<div class="cd"><div class="ct">🧠 Configurações GenAI</div>
<table><thead><tr><th>Modelo</th><th>Provider</th><th>Região</th><th>Serving</th><th>Default</th><th>Ações</th></tr></thead><tbody> <table><thead><tr><th>Nome</th><th>Modelo</th><th>Endpoint</th><th>Temp</th><th>Max Tokens</th><th>Ações</th></tr></thead><tbody>
${S.genaiCfg.map(g=>{const mi=S.models[g.model_id]||{};return`<tr><td><strong>${mi.name||g.model_id}</strong><br><span style="font-size:.63rem;color:var(--tm)">${g.model_id}</span></td> ${S.genaiCfg.map(g=>{const mi=S.models[g.model_id]||{};return`<tr><td><strong>${g.name||'—'}</strong>${g.is_default?' ':''}<br><span class="tag">${mi.provider||'?'}</span></td>
<td><span class="tag">${mi.provider||'?'}</span></td><td>${g.genai_region}</td><td>${g.serving_type}</td> <td>${mi.name||g.model_id}<br><span style="font-size:.6rem;color:var(--tm)">${g.model_ocid||g.model_id}</span></td>
<td>${g.is_default?'⭐':'—'}</td> <td style="font-size:.63rem;font-family:var(--fm)">${g.endpoint||'—'}</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="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:.65rem"><p>Nenhum modelo configurado. Adicione abaixo.</p></div>':''}</div> ${!S.genaiCfg.length?'<div class="emp" style="padding:.65rem"><p>Nenhum modelo configurado.</p></div>':''}</div>
<div class="cd"><div class="ct"> Novo Modelo GenAI</div><div id="gm"></div> <div class="cd"><div class="ct"> Novo Modelo GenAI</div>
<div class="g3"><div class="ig"><label>Config OCI (credenciais)</label><select id="goci"><option value="">Selecione...</option> <p style="font-size:.72rem;color:var(--tm);margin-bottom:.65rem">Configura conexão via <strong>OCI SDK</strong> com <code>GenerativeAiInferenceClient</code>. Endpoint é gerado automaticamente pela região.</p>
<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> ${S.ociCfg.map(c=>`<option value="${c.id}">${c.tenancy_name}</option>`).join('')}</select></div>
<div class="ig"><label>Modelo</label><select id="gmod">${mOpts}</select></div> <div class="ig"><label>Modelo (catálogo)</label><select id="gmod">${mOpts}</select></div></div>
<div class="ig"><label>Região GenAI</label><select id="greg">${rOpts}</select></div></div> <div class="g3"><div class="ig"><label>Região GenAI</label><select id="greg">${rOpts}</select></div>
<div class="g3"><div class="ig"><label>Compartment OCID</label><input type="text" id="gcmp" placeholder="ocid1.compartment.oc1.."></div> <div class="ig"><label>Compartment OCID</label><input type="text" id="gcmp" placeholder="ocid1.compartment.oc1.."></div>
<div class="ig"><label>Serving Type</label><select id="gsrv"><option value="ON_DEMAND">On-Demand</option><option value="DEDICATED">Dedicated</option></select></div> <div class="ig"><label>Model OCID (opcional)</label><div class="ht">Se diferente do catálogo (ex: modelo dedicado)</div><input type="text" id="gocid" placeholder="ocid1.generativeaimodel.oc1..."></div></div>
<div class="ig"><label>Endpoint OCID (dedicado)</label><input type="text" id="gep" placeholder="(opcional)"></div></div>
<div class="g3"><div class="ig"><label>Temperature</label><input type="number" id="gtemp" value="0.7" step="0.1" min="0" max="2"></div> <div style="font-size:.72rem;font-weight:600;color:var(--t2);margin:.5rem 0 .3rem">Parâmetros do Chat</div>
<div class="ig"><label>Max Tokens</label><input type="number" id="gmt" value="2048" min="1" max="128000"></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>Top P</label><input type="number" id="gtp" value="0.9" step="0.05" min="0" max="1"></div></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" onclick="sGenai()">Salvar Modelo</button></div>`} <button class="btn bp" onclick="sGenai()">Salvar Modelo</button></div>`}
async function sGenai(){try{await $api('/genai/config',{method:'POST',body:{ async function sGenai(){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, oci_config_id:document.getElementById('goci').value,model_id:document.getElementById('gmod').value,
model_ocid:document.getElementById('gocid').value||null,
compartment_id:document.getElementById('gcmp').value,genai_region:document.getElementById('greg').value, compartment_id:document.getElementById('gcmp').value,genai_region:document.getElementById('greg').value,
serving_type:document.getElementById('gsrv').value,endpoint_id:document.getElementById('gep').value||null, endpoint:null,
serving_type:'ON_DEMAND',dedicated_endpoint_id:null,
temperature:parseFloat(document.getElementById('gtemp').value),max_tokens:parseInt(document.getElementById('gmt').value), temperature:parseFloat(document.getElementById('gtemp').value),max_tokens:parseInt(document.getElementById('gmt').value),
top_p:parseFloat(document.getElementById('gtp').value),is_default:S.genaiCfg.length===0}}); 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!','s');R()}catch(e){sm('gm',e.message,'e')}} S.genaiCfg=await $api('/genai/configs');sm('gm','✅ Modelo salvo!','s');R()}catch(e){sm('gm',e.message,'e')}}
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)}} 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)}}
async function dGenai(id){if(!confirm('Excluir?'))return;await $api('/genai/configs/'+id,{method:'DELETE'});S.genaiCfg=await $api('/genai/configs');R()} async function dGenai(id){if(!confirm('Excluir?'))return;await $api('/genai/configs/'+id,{method:'DELETE'});S.genaiCfg=await $api('/genai/configs');R()}
// ── MCP Servers ── // ── MCP Servers ──
function rMCP(){return`<div class="cd"><div class="ct">🔌 MCP Servers Registrados</div> function rMCP(){const adbOpts=S.adbCfg.map(a=>`<option value="${a.id}">${a.config_name} (${a.dsn})</option>`).join('');
<table><thead><tr><th>Nome</th><th>Tipo</th><th>Comando/URL</th><th>Status</th><th>Ações</th></tr></thead><tbody> return`<div class="cd"><div class="ct">🔌 MCP Servers</div>
${S.mcpSvr.map(m=>`<tr><td><strong>${m.name}</strong><br><span style="font-size:.63rem;color:var(--tm)">${m.description||''}</span></td> <table><thead><tr><th>Nome</th><th>Tipo</th><th>Cmd/URL</th><th>ADB Link</th><th>Status</th><th>Ações</th></tr></thead><tbody>
${S.mcpSvr.map(m=>{const adb=m.linked_adb_id?S.adbCfg.find(a=>a.id===m.linked_adb_id):null;
return`<tr><td><strong>${m.name}</strong><br><span style="font-size:.63rem;color:var(--tm)">${m.description||''}</span></td>
<td><span class="tag">${m.server_type}</span></td> <td><span class="tag">${m.server_type}</span></td>
<td style="font-family:var(--fm);font-size:.65rem">${m.command||m.url||m.module_path||'—'}</td> <td style="font-family:var(--fm);font-size:.65rem">${m.command||m.url||m.module_path||'—'}</td>
<td>${adb?`<span class="badge b-pass">${adb.config_name}</span>`:'<span style="color:var(--tm);font-size:.65rem">—</span>'}</td>
<td><span class="badge ${m.is_active?'b-on':'b-off'}">${m.is_active?'Ativo':'Inativo'}</span></td> <td><span class="badge ${m.is_active?'b-on':'b-off'}">${m.is_active?'Ativo':'Inativo'}</span></td>
<td><button class="btn bs bsm" onclick="tgMcp('${m.id}')">${m.is_active?'Desativar':'Ativar'}</button> <td><button class="btn bs bsm" onclick="tgMcp('${m.id}')">${m.is_active?'Off':'On'}</button>
<button class="btn bd bsm" onclick="dMcp('${m.id}')">Excluir</button></td></tr>`).join('')}</tbody></table> <button class="btn bd bsm" onclick="dMcp('${m.id}')">Del</button></td></tr>`}).join('')}</tbody></table>
${!S.mcpSvr.length?'<div class="emp" style="padding:.65rem"><p>Nenhum MCP server. Registre abaixo para usar em relatórios.</p></div>':''}</div> ${!S.mcpSvr.length?'<div class="emp" style="padding:.65rem"><p>Nenhum MCP server.</p></div>':''}</div>
<div class="cd"><div class="ct"> Registrar MCP Server</div><div id="mcm"></div> <div class="cd"><div class="ct"> Registrar MCP Server</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="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="ig"><label>Descrição</label><input type="text" id="md" placeholder="Executa checks CIS 3.0"></div></div>
<div class="g3"><div class="ig"><label>Tipo</label><select id="mt"><option value="stdio">stdio</option><option value="sse">SSE (HTTP)</option><option value="module">Python Module</option></select></div> <div class="g3"><div class="ig"><label>Tipo</label><select id="mt"><option value="stdio">stdio</option><option value="sse">SSE (HTTP)</option><option value="module">Python Module</option></select></div>
<div class="ig"><label>Comando (stdio)</label><input type="text" id="mcmd" placeholder="python3 server.py"></div> <div class="ig"><label>Comando (stdio)</label><input type="text" id="mcmd" placeholder="python3 server.py"></div>
<div class="ig"><label>URL (SSE)</label><input type="text" id="murl" placeholder="http://localhost:8001/sse"></div></div> <div class="ig"><label>URL (SSE)</label><input type="text" id="murl" placeholder="http://localhost:8001/sse"></div></div>
<div class="ig"><label>Args (JSON array)</label><input type="text" id="margs" placeholder='["--config", "/path"]'></div> <div class="g2"><div class="ig"><label>🗄️ Vincular ADB Vector (tool)</label><div class="ht">MCP server usará este banco como ferramenta</div><select id="madb"><option value="">Nenhum</option>${adbOpts}</select></div>
<div class="ig"><label>Args (JSON array)</label><input type="text" id="margs" placeholder='["--config", "/path"]'></div></div>
<button class="btn bp" onclick="sMcp()">Registrar Server</button></div> <button class="btn bp" onclick="sMcp()">Registrar Server</button></div>
<div class="cd"><div class="ct">📤 Upload Script MCP</div> <div class="cd"><div class="ct">📤 Upload Script / 🔗 Link ADB</div>
<div class="g2"><div class="ig"><label>MCP Server</label><select id="mup">${S.mcpSvr.map(m=>`<option value="${m.id}">${m.name}</option>`).join('')}</select></div> <div class="g3"><div class="ig"><label>MCP Server</label><select id="mup">${S.mcpSvr.map(m=>`<option value="${m.id}">${m.name}</option>`).join('')}</select></div>
<div class="ig"><label>Arquivo .py</label><input type="file" id="muf" accept=".py"></div></div> <div class="ig"><label>Arquivo .py</label><input type="file" id="muf" accept=".py"></div>
<div class="ig"><label>Vincular ADB</label><select id="mlnk"><option value="">Nenhum</option>${adbOpts}</select>
<button class="btn bs bsm" style="margin-top:.3rem" onclick="lnkMcp()">Vincular</button></div></div>
<button class="btn bs" onclick="uMcp()">Upload Script</button></div>`} <button class="btn bs" onclick="uMcp()">Upload Script</button></div>`}
async function sMcp(){try{let args=null;const a=document.getElementById('margs').value;if(a)try{args=JSON.parse(a)}catch(e){} async function sMcp(){try{let args=null;const a=document.getElementById('margs').value;if(a)try{args=JSON.parse(a)}catch(e){}
await $api('/mcp/servers',{method:'POST',body:{name:document.getElementById('mn').value,description:document.getElementById('md').value, await $api('/mcp/servers',{method:'POST',body:{name:document.getElementById('mn').value,description:document.getElementById('md').value,
server_type:document.getElementById('mt').value,command:document.getElementById('mcmd').value||null,url:document.getElementById('murl').value||null,args}}); server_type:document.getElementById('mt').value,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')}} S.mcpSvr=await $api('/mcp/servers');sm('mcm','✅ Registrado!','s');R()}catch(e){sm('mcm',e.message,'e')}}
async function tgMcp(id){await $api('/mcp/servers/'+id+'/toggle',{method:'PUT'});S.mcpSvr=await $api('/mcp/servers');R()} 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?'))return;await $api('/mcp/servers/'+id,{method:'DELETE'});S.mcpSvr=await $api('/mcp/servers');R()} async function dMcp(id){if(!confirm('Excluir?'))return;await $api('/mcp/servers/'+id,{method:'DELETE'});S.mcpSvr=await $api('/mcp/servers');R()}
async function uMcp(){const fd=new FormData();const f=document.getElementById('muf').files[0];if(!f)return alert('Selecione um arquivo'); async function uMcp(){const fd=new FormData();const f=document.getElementById('muf').files[0];if(!f)return alert('Selecione um arquivo');
fd.append('file',f);const id=document.getElementById('mup').value;if(!id)return alert('Selecione um server'); fd.append('file',f);const id=document.getElementById('mup').value;if(!id)return alert('Selecione um server');
try{await $api('/mcp/servers/'+id+'/upload',{method:'POST',body:fd,headers:{}});alert('Upload OK!')}catch(e){alert(e.message)}} try{await $api('/mcp/servers/'+id+'/upload',{method:'POST',body:fd,headers:{}});alert('Upload OK!')}catch(e){alert(e.message)}}
async function lnkMcp(){const mid=document.getElementById('mup').value;const aid=document.getElementById('mlnk').value;if(!mid)return alert('Selecione MCP');
try{await $api('/mcp/servers/'+mid+'/link-adb?adb_id='+(aid||''),{method:'PUT'});S.mcpSvr=await $api('/mcp/servers');alert('Vinculado!');R()}catch(e){alert(e.message)}}
// ── ADB Vector ── // ── ADB Vector ──
function rADB(){return`<div class="cd"><div class="ct">🗄️ Conexões Autonomous Database (Vector)</div> function rADB(){return`<div class="cd"><div class="ct">🗄️ Conexões Autonomous Database (Vector)</div>
@@ -307,13 +341,12 @@ ${S.adbCfg.map(c=>`<tr><td><strong>${c.config_name}</strong></td><td style="font
<td><button class="btn bs bsm" onclick="tAdb('${c.id}')">Testar</button> <button class="btn bd bsm" onclick="dAdb('${c.id}')">Excluir</button></td></tr>`).join('')}</tbody></table> <td><button class="btn bs bsm" onclick="tAdb('${c.id}')">Testar</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:.65rem"><p>Nenhuma conexão ADB.</p></div>':''}</div> ${!S.adbCfg.length?'<div class="emp" style="padding:.65rem"><p>Nenhuma conexão ADB.</p></div>':''}</div>
<div class="cd"><div class="ct"> Nova Conexão Autonomous DB</div> <div class="cd"><div class="ct"> Nova Conexão Autonomous DB</div>
<p style="font-size:.76rem;color:var(--tm);margin-bottom:.65rem">Conexão via <strong>python-oracledb</strong> (Thin mode) com Wallet (mTLS) para Oracle Autonomous Database.</p> <p style="font-size:.72rem;color:var(--tm);margin-bottom:.65rem">Conexão via <strong>python-oracledb</strong> Thin mode com Wallet (mTLS) para Oracle Autonomous Database.</p><div id="am"></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="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 (do tnsnames.ora)</div><input type="text" id="adsn" placeholder="myatp_high"></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>Username</label><input type="text" id="auser" placeholder="ADMIN"></div>
<div class="ig"><label>Password</label><input type="password" id="apw" placeholder="••••••••"></div> <div class="ig"><label>Password</label><input type="password" id="apw"></div>
<div class="ig"><label>Wallet Password</label><div class="ht">Senha do ewallet.pem (definida ao baixar wallet)</div><input type="password" id="awpw" placeholder="(opcional)"></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> <div class="ig"><label>Tabela de Embeddings</label><input type="text" id="atbl" value="CIS_EMBEDDINGS"></div></div>
<button class="btn bp" onclick="sAdb()">Salvar Conexão</button></div> <button class="btn bp" onclick="sAdb()">Salvar Conexão</button></div>
<div class="cd"><div class="ct">📤 Upload Wallet (.zip)</div> <div class="cd"><div class="ct">📤 Upload Wallet (.zip)</div>

21
logo.svg Normal file
View File

@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="128" height="128">
<!-- Oracle-style rounded rectangle -->
<rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(199,70,52,0.08)" stroke="#C74634" stroke-width="2"/>
<!-- Robot head -->
<rect x="11" y="11" width="14" height="14" rx="4" fill="#fff" stroke="#C74634" stroke-width="0.5"/>
<!-- Eyes -->
<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"/>
<!-- Mouth -->
<rect x="14" y="21" width="8" height="1.5" rx="0.75" fill="#C74634" opacity="0.6"/>
<!-- Antenna -->
<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.3" stroke="#C74634" stroke-width="0.8"/>
<!-- Ears/signal -->
<line x1="11" y1="18" x2="6" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.5"/>
<line x1="25" y1="18" x2="30" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.5"/>
<circle cx="5.5" cy="18" r="1" fill="#C74634" opacity="0.4"/>
<circle cx="30.5" cy="18" r="1" fill="#C74634" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB