feat: session token auth, workspace submenu, retry buttons, terraform fixes, app rename
- Rename app to "AI Agent - Infrastructure & Security Engineer" - OCI session token authentication (SecurityTokenSigner, conditional form, type tags) - Terraform Workspaces sidebar sub-tab (date-grouped accordion, INNER JOIN filter) - Retry button on failed messages (Chat, Terraform, Prompt Generator) - Fix f2 undefined in dedup, oci_core_services false validation, Re-plan tfCode sync - Auto-fix button no longer requires pre-selected model - DRG attachment rules added to Terraform system prompt - Stuck workspace recovery on container restart
This commit is contained in:
@@ -1,15 +1,15 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="logo.svg" alt="OCI CIS AI Agent" width="96" height="96">
|
<img src="logo.svg" alt="AI Agent - Infrastructure & Security Engineer" width="96" height="96">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">OCI CIS AI Agent</h1>
|
<h1 align="center">AI Agent — Infrastructure & Security Engineer</h1>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>Oracle Cloud Infrastructure — CIS Foundations Benchmark 3.0 — AI-Powered Compliance Platform</strong>
|
<strong>Oracle Cloud Infrastructure — CIS Foundations Benchmark 3.0 — AI-Powered Compliance Platform</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/badge/version-2.5-C74634?style=flat-square" alt="Version">
|
<img src="https://img.shields.io/badge/version-2.6-C74634?style=flat-square" alt="Version">
|
||||||
<img src="https://img.shields.io/badge/python-3.12-3776AB?style=flat-square" alt="Python">
|
<img src="https://img.shields.io/badge/python-3.12-3776AB?style=flat-square" alt="Python">
|
||||||
<img src="https://img.shields.io/badge/FastAPI-0.115-009688?style=flat-square" alt="FastAPI">
|
<img src="https://img.shields.io/badge/FastAPI-0.115-009688?style=flat-square" alt="FastAPI">
|
||||||
<img src="https://img.shields.io/badge/OCI-GenAI-C74634?style=flat-square" alt="OCI">
|
<img src="https://img.shields.io/badge/OCI-GenAI-C74634?style=flat-square" alt="OCI">
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
OCI CIS AI Agent is a self-hosted web application that automates **CIS Oracle Cloud Infrastructure Foundations Benchmark 3.0** compliance checks, powered by **OCI Generative AI** for intelligent analysis and an **MCP (Model Context Protocol)** server architecture for extensible task execution.
|
AI Agent — Infrastructure & Security Engineer is a self-hosted web application that automates **CIS Oracle Cloud Infrastructure Foundations Benchmark 3.0** compliance checks, powered by **OCI Generative AI** for intelligent analysis and an **MCP (Model Context Protocol)** server architecture for extensible task execution.
|
||||||
|
|
||||||
The platform combines security compliance scanning, AI-powered chat with **RAG (Retrieval-Augmented Generation)**, infrastructure exploration, and vector-based knowledge storage into a single, containerized solution with an **Oracle Dark Premium** theme (light/dark modes), **KPI dashboard** with compliance gauge, and **Chart.js** visualizations.
|
The platform combines security compliance scanning, AI-powered chat with **RAG (Retrieval-Augmented Generation)**, infrastructure exploration, and vector-based knowledge storage into a single, containerized solution with an **Oracle Dark Premium** theme (light/dark modes), **KPI dashboard** with compliance gauge, and **Chart.js** visualizations.
|
||||||
|
|
||||||
@@ -697,6 +697,7 @@ All models include OCID mapping for `us-ashburn-1`. For other regions, use the "
|
|||||||
|
|
||||||
| Version | Date | Changes |
|
| Version | Date | Changes |
|
||||||
|---------|------|---------|
|
|---------|------|---------|
|
||||||
|
| **v2.6** | 2026-03 | **Rename**: app renamed to "AI Agent — Infrastructure & Security Engineer". **Session Token Auth**: OCI session token authentication support (type selector API Key/Session Token, conditional form fields, collapsible credential form, type tags in credentials table, `SecurityTokenSigner` backend). **Terraform Workspaces submenu**: sidebar sub-tab listing all workspaces grouped by date with accordion UI, filtered by existing chat sessions (INNER JOIN), actions (plan/apply/destroy/delete/open). **Retry on failure**: "Reenviar" button on failed messages across Chat Agent, Terraform Agent, and Prompt Generator. **Terraform fixes**: `f2` undefined variable in dedup logic, `oci_core_services` false positive validation (data sources excluded), `S.tfCode` reconstruction from `S.tfFiles` for Re-plan, auto-fix button visible without pre-selected model, DRG attachment rules in system prompt. **Resilience**: stuck workspace recovery on container restart (planning/applying/destroying → reset + lock file cleanup). |
|
||||||
| **v2.5** | 2026-03 | **Explorer Expansion**: KPI stats bar with per-category resource counts (cached in SQLite `explorer_counts_cache`, background refresh on compartment access), enhanced views for VCNs/Subnets/Load Balancers/Buckets with expandable cards, start/stop for DB Systems + MySQL + Container Instances (5 resource types total), dead state filtering (`TERMINATED`/`DELETED`/`DELETING` hidden across all 36 endpoints), wider compartment tree (280px), IAM excluded from compartment totals. **Prompt Generator Intelligence**: same knowledge pipeline as Terraform Agent — auto-detects resource types from user message, fetches official docs (Example Usage + Arguments) from GitHub cached in SQLite, uses compact categorized resource reference. **Network Firewall fix**: `NetworkFirewallPolicySummaryCollection` not iterable — added `.items` accessor for collection objects. |
|
| **v2.5** | 2026-03 | **Explorer Expansion**: KPI stats bar with per-category resource counts (cached in SQLite `explorer_counts_cache`, background refresh on compartment access), enhanced views for VCNs/Subnets/Load Balancers/Buckets with expandable cards, start/stop for DB Systems + MySQL + Container Instances (5 resource types total), dead state filtering (`TERMINATED`/`DELETED`/`DELETING` hidden across all 36 endpoints), wider compartment tree (280px), IAM excluded from compartment totals. **Prompt Generator Intelligence**: same knowledge pipeline as Terraform Agent — auto-detects resource types from user message, fetches official docs (Example Usage + Arguments) from GitHub cached in SQLite, uses compact categorized resource reference. **Network Firewall fix**: `NetworkFirewallPolicySummaryCollection` not iterable — added `.items` accessor for collection objects. |
|
||||||
| **v2.4** | 2026-03 | **Terraform Safety & UX**: region safety guard (plan blocked if model omits `variable "region"` — prevents wrong-region provisioning), rollback button on apply failure (manual `terraform destroy` with "ROLLBACK" confirmation), SSH public key field in OCI configs (auto-injected into `terraform.tfvars` for compute instances), reasoning_effort uppercase fix for OCI SDK, split-panel layout (terminal right 40%, files/plan/resources below chat 60%), plan button persists after destroy. **Terraform tfvars**: expanded `oci_var_map` with `ssh_public_key`/`ssh_authorized_keys` auto-mapping from OCI config |
|
| **v2.4** | 2026-03 | **Terraform Safety & UX**: region safety guard (plan blocked if model omits `variable "region"` — prevents wrong-region provisioning), rollback button on apply failure (manual `terraform destroy` with "ROLLBACK" confirmation), SSH public key field in OCI configs (auto-injected into `terraform.tfvars` for compute instances), reasoning_effort uppercase fix for OCI SDK, split-panel layout (terminal right 40%, files/plan/resources below chat 60%), plan button persists after destroy. **Terraform tfvars**: expanded `oci_var_map` with `ssh_public_key`/`ssh_authorized_keys` auto-mapping from OCI config |
|
||||||
| **v2.3** | 2026-03 | **Consult Embeddings**: dedicated sub-menu with chat-like interface for natural language Q&A against vector store — queries all active tables via cosine similarity, returns contextual answers with source citations (works with or without GenAI config). **Knowledge Base & RAG Enhancements**: Knowledge Base (Base de Conhecimento) with file upload + URL import to `engineerknowledgebase` vector table, CIS Recommendations upload to `cisrecom` table, URL content extraction (HTML tag/script stripping, remote PDF parsing), auto-resolve embedding config from OCI credentials (no separate GenAI config required), `report_date` metadata tracking for embedding freshness, purge & re-embed flow for updated reports. **ADB Vector Improvements**: case-insensitive table matching with quoted identifiers for Oracle ADB, table validation endpoint against `user_tables`, tables section moved inside edit connection card, case-preserving table registration (removed forced uppercase). **Vector search fix**: FLOAT32 compatibility for VECTOR_DISTANCE queries, base64-decoded compartment_id for auto-resolved OCI configs. **Embeddings tab cleanup**: removed delete buttons, simplified to read-only view, updated descriptions. **Dependencies**: added PyPDF2 for PDF text extraction |
|
| **v2.3** | 2026-03 | **Consult Embeddings**: dedicated sub-menu with chat-like interface for natural language Q&A against vector store — queries all active tables via cosine similarity, returns contextual answers with source citations (works with or without GenAI config). **Knowledge Base & RAG Enhancements**: Knowledge Base (Base de Conhecimento) with file upload + URL import to `engineerknowledgebase` vector table, CIS Recommendations upload to `cisrecom` table, URL content extraction (HTML tag/script stripping, remote PDF parsing), auto-resolve embedding config from OCI credentials (no separate GenAI config required), `report_date` metadata tracking for embedding freshness, purge & re-embed flow for updated reports. **ADB Vector Improvements**: case-insensitive table matching with quoted identifiers for Oracle ADB, table validation endpoint against `user_tables`, tables section moved inside edit connection card, case-preserving table registration (removed forced uppercase). **Vector search fix**: FLOAT32 compatibility for VECTOR_DISTANCE queries, base64-decoded compartment_id for auto-resolved OCI configs. **Embeddings tab cleanup**: removed delete buttons, simplified to read-only view, updated descriptions. **Dependencies**: added PyPDF2 for PDF text extraction |
|
||||||
|
|||||||
172
backend/app.py
172
backend/app.py
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
OCI CIS AI Agent - Backend API v1.1
|
AI Agent - Infrastructure & Security Engineer - Backend API v1.1
|
||||||
FastAPI with JWT auth, TOTP MFA, RBAC, OCI GenAI (exact SDK pattern),
|
FastAPI with JWT auth, TOTP MFA, RBAC, OCI GenAI (exact SDK pattern),
|
||||||
OCI Account Explorer, MCP Server registry with VectorDB tool integration,
|
OCI Account Explorer, MCP Server registry with VectorDB tool integration,
|
||||||
Autonomous DB vector storage, CIS reports, chat agent, audit log.
|
Autonomous DB vector storage, CIS reports, chat agent, audit log.
|
||||||
@@ -52,7 +52,7 @@ COMPACT_MIN_MESSAGES = 8
|
|||||||
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=VERSION)
|
app = FastAPI(title="AI Agent - Infrastructure & Security Engineer", 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()
|
||||||
@@ -390,7 +390,7 @@ def init_db():
|
|||||||
c.execute(f"ALTER TABLE terraform_workspaces ADD COLUMN {col}")
|
c.execute(f"ALTER TABLE terraform_workspaces ADD COLUMN {col}")
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
pass
|
pass
|
||||||
for col in ["ssh_public_key TEXT"]:
|
for col in ["ssh_public_key TEXT", "auth_type TEXT DEFAULT 'api_key'"]:
|
||||||
try:
|
try:
|
||||||
c.execute(f"ALTER TABLE oci_configs ADD COLUMN {col}")
|
c.execute(f"ALTER TABLE oci_configs ADD COLUMN {col}")
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
@@ -427,6 +427,16 @@ def init_db():
|
|||||||
else:
|
else:
|
||||||
c.execute("INSERT INTO system_prompts (id,name,agent,content,is_active) VALUES (?,?,?,?,?)",
|
c.execute("INSERT INTO system_prompts (id,name,agent,content,is_active) VALUES (?,?,?,?,?)",
|
||||||
(str(uuid.uuid4()), "OCI Terraform Agent", "terraform", TF_DEFAULT_SYSTEM_PROMPT, 1))
|
(str(uuid.uuid4()), "OCI Terraform Agent", "terraform", TF_DEFAULT_SYSTEM_PROMPT, 1))
|
||||||
|
# Recover stuck terraform workspaces (interrupted by container restart)
|
||||||
|
stuck = c.execute("SELECT id, status FROM terraform_workspaces WHERE status IN ('planning','applying','destroying')").fetchall()
|
||||||
|
for ws in stuck:
|
||||||
|
prev = 'applied' if ws["status"] == 'destroying' else 'failed'
|
||||||
|
c.execute("UPDATE terraform_workspaces SET status=?, error='Interrompido por restart do container', updated_at=datetime('now') WHERE id=?", (prev, ws["id"]))
|
||||||
|
# Remove stale lock files
|
||||||
|
lock_file = TERRAFORM_DIR / ws["id"] / ".terraform.tfstate.lock.info"
|
||||||
|
if lock_file.exists():
|
||||||
|
lock_file.unlink()
|
||||||
|
log.info(f"Recovered stuck workspace {ws['id']}: {ws['status']} -> {prev}")
|
||||||
adm = c.execute("SELECT id FROM users WHERE username='admin'").fetchone()
|
adm = c.execute("SELECT id FROM users WHERE username='admin'").fetchone()
|
||||||
if not adm:
|
if not adm:
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -446,7 +456,7 @@ def _totp_verify(secret,code,window=1):
|
|||||||
c=str((struct.unpack(">I",h[o:o+4])[0]&0x7FFFFFFF)%1_000_000).zfill(6)
|
c=str((struct.unpack(">I",h[o:o+4])[0]&0x7FFFFFFF)%1_000_000).zfill(6)
|
||||||
if hmac.compare_digest(c,code): return True
|
if hmac.compare_digest(c,code): return True
|
||||||
return False
|
return False
|
||||||
def _totp_uri(secret,user): return f"otpauth://totp/OCI-CIS-Agent:{user}?secret={secret}&issuer=OCI-CIS-Agent"
|
def _totp_uri(secret,user): return f"otpauth://totp/AI-Agent:{user}?secret={secret}&issuer=AI-Agent"
|
||||||
def _make_token(uid,role,sid):
|
def _make_token(uid,role,sid):
|
||||||
return pyjwt.encode({"sub":uid,"role":role,"sid":sid,"exp":datetime.utcnow()+timedelta(hours=JWT_EXP_H),"iat":datetime.utcnow()},APP_SECRET,algorithm=JWT_ALG)
|
return pyjwt.encode({"sub":uid,"role":role,"sid":sid,"exp":datetime.utcnow()+timedelta(hours=JWT_EXP_H),"iat":datetime.utcnow()},APP_SECRET,algorithm=JWT_ALG)
|
||||||
def _enc(v): return base64.b64encode(v.encode()).decode()
|
def _enc(v): return base64.b64encode(v.encode()).decode()
|
||||||
@@ -469,7 +479,30 @@ def _safe_dec(v):
|
|||||||
def _get_oci_config(oci_config_id: str) -> dict:
|
def _get_oci_config(oci_config_id: str) -> dict:
|
||||||
import oci
|
import oci
|
||||||
config_path = str(OCI_DIR / oci_config_id / "config")
|
config_path = str(OCI_DIR / oci_config_id / "config")
|
||||||
return oci.config.from_file(config_path, "DEFAULT")
|
config = oci.config.from_file(config_path, "DEFAULT")
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def _get_oci_signer(oci_config_id: str):
|
||||||
|
"""Return (config, signer) tuple. For session_token auth, returns SecurityTokenSigner; for api_key returns None (use default)."""
|
||||||
|
import oci
|
||||||
|
config = _get_oci_config(oci_config_id)
|
||||||
|
if config.get("security_token_file"):
|
||||||
|
from oci.auth.signers import SecurityTokenSigner
|
||||||
|
token_path = config["security_token_file"]
|
||||||
|
with open(token_path) as f:
|
||||||
|
token = f.read().strip()
|
||||||
|
signer = SecurityTokenSigner(token, config["key_file"])
|
||||||
|
return config, signer
|
||||||
|
return config, None
|
||||||
|
|
||||||
|
|
||||||
|
def _oci_client(client_class, oci_config_id: str, **kwargs):
|
||||||
|
"""Create an OCI client with correct auth (api_key or session_token)."""
|
||||||
|
config, signer = _get_oci_signer(oci_config_id)
|
||||||
|
if signer:
|
||||||
|
return client_class(config=config, signer=signer, **kwargs)
|
||||||
|
return client_class(config, **kwargs)
|
||||||
|
|
||||||
# ── Auth deps ─────────────────────────────────────────────────────────────────
|
# ── Auth deps ─────────────────────────────────────────────────────────────────
|
||||||
async def current_user(cred: HTTPAuthorizationCredentials = Depends(security)):
|
async def current_user(cred: HTTPAuthorizationCredentials = Depends(security)):
|
||||||
@@ -653,43 +686,79 @@ async def del_user(uid: str, adm=Depends(require("admin"))):
|
|||||||
@app.post("/api/oci/config")
|
@app.post("/api/oci/config")
|
||||||
async def save_oci(
|
async def save_oci(
|
||||||
tenancy_name: str = Form(...), tenancy_ocid: str = Form(...),
|
tenancy_name: str = Form(...), tenancy_ocid: str = Form(...),
|
||||||
user_ocid: str = Form(...), fingerprint: str = Form(...),
|
user_ocid: str = Form(""), fingerprint: str = Form(""),
|
||||||
region: str = Form(...), compartment_id: str = Form(""),
|
region: str = Form(...), compartment_id: str = Form(""),
|
||||||
key_passphrase: str = Form(""),
|
key_passphrase: str = Form(""),
|
||||||
ssh_public_key: str = Form(""),
|
ssh_public_key: str = Form(""),
|
||||||
private_key: UploadFile = File(...), public_key: Optional[UploadFile] = File(None),
|
auth_type: str = Form("api_key"),
|
||||||
|
security_token: str = Form(""),
|
||||||
|
private_key: Optional[UploadFile] = File(None), public_key: Optional[UploadFile] = File(None),
|
||||||
u = Depends(require("admin","user"))
|
u = Depends(require("admin","user"))
|
||||||
):
|
):
|
||||||
|
if auth_type not in ("api_key", "session_token"):
|
||||||
|
raise HTTPException(400, "auth_type deve ser 'api_key' ou 'session_token'")
|
||||||
cid = str(uuid.uuid4()); cdir = OCI_DIR / cid; cdir.mkdir(parents=True)
|
cid = str(uuid.uuid4()); cdir = OCI_DIR / cid; cdir.mkdir(parents=True)
|
||||||
kp = cdir / "oci_api_key.pem"
|
kp = cdir / "oci_api_key.pem"
|
||||||
|
|
||||||
|
if auth_type == "session_token":
|
||||||
|
# Session token auth: requires tenancy_ocid, region, token, and session key
|
||||||
|
if not security_token.strip():
|
||||||
|
shutil.rmtree(cdir, ignore_errors=True)
|
||||||
|
raise HTTPException(400, "Session Token é obrigatório para autenticação por token.")
|
||||||
|
if not private_key or not private_key.filename:
|
||||||
|
shutil.rmtree(cdir, ignore_errors=True)
|
||||||
|
raise HTTPException(400, "A chave de sessão (.pem) é obrigatória para autenticação por token.")
|
||||||
|
key_bytes = await private_key.read()
|
||||||
|
kp.write_bytes(key_bytes); kp.chmod(0o600)
|
||||||
|
# Save security token file
|
||||||
|
token_file = cdir / "token"
|
||||||
|
token_file.write_text(security_token.strip())
|
||||||
|
token_file.chmod(0o600)
|
||||||
|
# Generate config
|
||||||
|
cfg_content = (f"[DEFAULT]\ntenancy={tenancy_ocid}\nregion={region}\n"
|
||||||
|
f"key_file={kp}\nsecurity_token_file={token_file}\n")
|
||||||
|
if user_ocid:
|
||||||
|
cfg_content = f"[DEFAULT]\nuser={user_ocid}\n" + cfg_content.replace("[DEFAULT]\n", "")
|
||||||
|
cfg_file = cdir / "config"
|
||||||
|
cfg_file.write_text(cfg_content); cfg_file.chmod(0o600)
|
||||||
|
# For session_token, fingerprint/user may not be provided — use placeholders
|
||||||
|
user_ocid = user_ocid or "session_token_user"
|
||||||
|
fingerprint = fingerprint or "session_token"
|
||||||
|
else:
|
||||||
|
# API Key auth (original flow)
|
||||||
|
if not user_ocid or not fingerprint:
|
||||||
|
shutil.rmtree(cdir, ignore_errors=True)
|
||||||
|
raise HTTPException(400, "OCID User e Fingerprint são obrigatórios para API Key.")
|
||||||
|
if not private_key or not private_key.filename:
|
||||||
|
shutil.rmtree(cdir, ignore_errors=True)
|
||||||
|
raise HTTPException(400, "Selecione a chave privada (.pem).")
|
||||||
key_bytes = await private_key.read()
|
key_bytes = await private_key.read()
|
||||||
kp.write_bytes(key_bytes); kp.chmod(0o600)
|
kp.write_bytes(key_bytes); kp.chmod(0o600)
|
||||||
# Detect encrypted keys that require passphrase
|
|
||||||
key_text = key_bytes.decode("utf-8", errors="ignore")
|
key_text = key_bytes.decode("utf-8", errors="ignore")
|
||||||
key_is_encrypted = "ENCRYPTED" in key_text or "Proc-Type: 4,ENCRYPTED" in key_text
|
key_is_encrypted = "ENCRYPTED" in key_text or "Proc-Type: 4,ENCRYPTED" in key_text
|
||||||
if key_is_encrypted and not key_passphrase:
|
if key_is_encrypted and not key_passphrase:
|
||||||
# Clean up and warn
|
|
||||||
shutil.rmtree(cdir, ignore_errors=True)
|
shutil.rmtree(cdir, ignore_errors=True)
|
||||||
raise HTTPException(400, "A chave privada está criptografada (ENCRYPTED). Informe a Key Passphrase.")
|
raise HTTPException(400, "A chave privada está criptografada (ENCRYPTED). Informe a Key Passphrase.")
|
||||||
if public_key: (cdir / "oci_api_key_public.pem").write_bytes(await public_key.read())
|
if public_key and public_key.filename:
|
||||||
|
(cdir / "oci_api_key_public.pem").write_bytes(await public_key.read())
|
||||||
cfg_file = cdir / "config"
|
cfg_file = cdir / "config"
|
||||||
cfg_content = (f"[DEFAULT]\nuser={user_ocid}\nfingerprint={fingerprint}\n"
|
cfg_content = (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")
|
||||||
if key_passphrase:
|
if key_passphrase:
|
||||||
cfg_content += f"pass_phrase={key_passphrase}\n"
|
cfg_content += f"pass_phrase={key_passphrase}\n"
|
||||||
cfg_file.write_text(cfg_content)
|
cfg_file.write_text(cfg_content); cfg_file.chmod(0o600)
|
||||||
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,ssh_public_key) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
c.execute("INSERT INTO oci_configs (id,user_id,tenancy_name,tenancy_ocid,user_ocid,fingerprint,region,key_file_path,compartment_id,ssh_public_key,auth_type) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
|
||||||
(cid, u["id"], tenancy_name, _enc(tenancy_ocid), _enc(user_ocid), _enc(fingerprint), region, str(kp), _enc(compartment_id) if compartment_id else None, ssh_public_key.strip() if ssh_public_key.strip() else None))
|
(cid, u["id"], tenancy_name, _enc(tenancy_ocid), _enc(user_ocid), _enc(fingerprint), region, str(kp), _enc(compartment_id) if compartment_id else None, ssh_public_key.strip() if ssh_public_key.strip() else None, auth_type))
|
||||||
_audit(u["id"], u["username"], "save_oci_config", cid, f"tenancy={tenancy_name}")
|
_audit(u["id"], u["username"], "save_oci_config", cid, f"tenancy={tenancy_name} auth={auth_type}")
|
||||||
_config_log("oci", cid, tenancy_name, "success", "save", f"Credencial salva: {tenancy_name} ({region})", u["id"], u["username"])
|
_config_log("oci", cid, tenancy_name, "success", "save", f"Credencial salva: {tenancy_name} ({region}) [{auth_type}]", u["id"], u["username"])
|
||||||
return {"id": cid, "tenancy_name": tenancy_name, "region": region}
|
return {"id": cid, "tenancy_name": tenancy_name, "region": region}
|
||||||
|
|
||||||
@app.get("/api/oci/configs")
|
@app.get("/api/oci/configs")
|
||||||
async def list_oci(u=Depends(current_user)):
|
async def list_oci(u=Depends(current_user)):
|
||||||
with db() as c:
|
with db() as c:
|
||||||
cols = "id,user_id,tenancy_name,tenancy_ocid,user_ocid,fingerprint,region,compartment_id,ssh_public_key,created_at"
|
cols = "id,user_id,tenancy_name,tenancy_ocid,user_ocid,fingerprint,region,compartment_id,ssh_public_key,auth_type,created_at"
|
||||||
if u["role"]=="admin": rows=c.execute(f"SELECT {cols} FROM oci_configs").fetchall()
|
if u["role"]=="admin": rows=c.execute(f"SELECT {cols} FROM oci_configs").fetchall()
|
||||||
else: rows=c.execute(f"SELECT {cols} FROM oci_configs WHERE user_id=?",(u["id"],)).fetchall()
|
else: rows=c.execute(f"SELECT {cols} FROM oci_configs WHERE user_id=?",(u["id"],)).fetchall()
|
||||||
result = []
|
result = []
|
||||||
@@ -736,6 +805,8 @@ async def update_oci(
|
|||||||
region: str = Form(...), compartment_id: str = Form(""),
|
region: str = Form(...), compartment_id: str = Form(""),
|
||||||
key_passphrase: str = Form(""),
|
key_passphrase: str = Form(""),
|
||||||
ssh_public_key: str = Form(""),
|
ssh_public_key: str = Form(""),
|
||||||
|
auth_type: str = Form(""),
|
||||||
|
security_token: str = Form(""),
|
||||||
private_key: Optional[UploadFile] = File(None), public_key: Optional[UploadFile] = File(None),
|
private_key: Optional[UploadFile] = File(None), public_key: Optional[UploadFile] = File(None),
|
||||||
u = Depends(require("admin","user"))
|
u = Depends(require("admin","user"))
|
||||||
):
|
):
|
||||||
@@ -748,6 +819,11 @@ async def update_oci(
|
|||||||
user_ocid = user_ocid or _safe_dec(existing["user_ocid"])
|
user_ocid = user_ocid or _safe_dec(existing["user_ocid"])
|
||||||
fingerprint = fingerprint or _safe_dec(existing["fingerprint"])
|
fingerprint = fingerprint or _safe_dec(existing["fingerprint"])
|
||||||
compartment_id = compartment_id or _safe_dec(existing["compartment_id"]) or ""
|
compartment_id = compartment_id or _safe_dec(existing["compartment_id"]) or ""
|
||||||
|
try:
|
||||||
|
existing_auth = existing["auth_type"] or "api_key"
|
||||||
|
except (IndexError, KeyError):
|
||||||
|
existing_auth = "api_key"
|
||||||
|
auth_type = auth_type or existing_auth
|
||||||
try:
|
try:
|
||||||
existing_ssh = existing["ssh_public_key"] or ""
|
existing_ssh = existing["ssh_public_key"] or ""
|
||||||
except (IndexError, KeyError):
|
except (IndexError, KeyError):
|
||||||
@@ -759,21 +835,32 @@ async def update_oci(
|
|||||||
key_bytes = await private_key.read()
|
key_bytes = await private_key.read()
|
||||||
kp.write_bytes(key_bytes); kp.chmod(0o600)
|
kp.write_bytes(key_bytes); kp.chmod(0o600)
|
||||||
key_text = key_bytes.decode("utf-8", errors="ignore")
|
key_text = key_bytes.decode("utf-8", errors="ignore")
|
||||||
if ("ENCRYPTED" in key_text or "Proc-Type: 4,ENCRYPTED" in key_text) and not key_passphrase:
|
if auth_type == "api_key" and ("ENCRYPTED" in key_text or "Proc-Type: 4,ENCRYPTED" in key_text) and not key_passphrase:
|
||||||
raise HTTPException(400, "A chave privada está criptografada (ENCRYPTED). Informe a Key Passphrase.")
|
raise HTTPException(400, "A chave privada está criptografada (ENCRYPTED). Informe a Key Passphrase.")
|
||||||
if public_key and public_key.filename:
|
if public_key and public_key.filename:
|
||||||
(cdir / "oci_api_key_public.pem").write_bytes(await public_key.read())
|
(cdir / "oci_api_key_public.pem").write_bytes(await public_key.read())
|
||||||
|
# Update session token file if provided
|
||||||
|
if security_token.strip():
|
||||||
|
token_file = cdir / "token"
|
||||||
|
token_file.write_text(security_token.strip()); token_file.chmod(0o600)
|
||||||
cfg_file = cdir / "config"
|
cfg_file = cdir / "config"
|
||||||
|
if auth_type == "session_token":
|
||||||
|
token_path = cdir / "token"
|
||||||
|
cfg_content = f"[DEFAULT]\ntenancy={tenancy_ocid}\nregion={region}\nkey_file={kp}\n"
|
||||||
|
if user_ocid and user_ocid != "session_token_user":
|
||||||
|
cfg_content = f"[DEFAULT]\nuser={user_ocid}\ntenancy={tenancy_ocid}\nregion={region}\nkey_file={kp}\n"
|
||||||
|
cfg_content += f"security_token_file={token_path}\n"
|
||||||
|
else:
|
||||||
cfg_content = (f"[DEFAULT]\nuser={user_ocid}\nfingerprint={fingerprint}\n"
|
cfg_content = (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")
|
||||||
if key_passphrase:
|
if key_passphrase:
|
||||||
cfg_content += f"pass_phrase={key_passphrase}\n"
|
cfg_content += f"pass_phrase={key_passphrase}\n"
|
||||||
cfg_file.write_text(cfg_content); cfg_file.chmod(0o600)
|
cfg_file.write_text(cfg_content); cfg_file.chmod(0o600)
|
||||||
with db() as c:
|
with db() as c:
|
||||||
c.execute("UPDATE oci_configs SET tenancy_name=?,tenancy_ocid=?,user_ocid=?,fingerprint=?,region=?,compartment_id=?,ssh_public_key=? WHERE id=?",
|
c.execute("UPDATE oci_configs SET tenancy_name=?,tenancy_ocid=?,user_ocid=?,fingerprint=?,region=?,compartment_id=?,ssh_public_key=?,auth_type=? WHERE id=?",
|
||||||
(tenancy_name, _enc(tenancy_ocid), _enc(user_ocid), _enc(fingerprint), region, _enc(compartment_id) if compartment_id else None, ssh_public_key or None, cid))
|
(tenancy_name, _enc(tenancy_ocid), _enc(user_ocid), _enc(fingerprint), region, _enc(compartment_id) if compartment_id else None, ssh_public_key or None, auth_type, cid))
|
||||||
_audit(u["id"], u["username"], "update_oci_config", cid, f"tenancy={tenancy_name}")
|
_audit(u["id"], u["username"], "update_oci_config", cid, f"tenancy={tenancy_name} auth={auth_type}")
|
||||||
_config_log("oci", cid, tenancy_name, "success", "save", f"Credencial atualizada: {tenancy_name} ({region})", u["id"], u["username"])
|
_config_log("oci", cid, tenancy_name, "success", "save", f"Credencial atualizada: {tenancy_name} ({region}) [{auth_type}]", u["id"], u["username"])
|
||||||
return {"id": cid, "tenancy_name": tenancy_name, "region": region}
|
return {"id": cid, "tenancy_name": tenancy_name, "region": region}
|
||||||
|
|
||||||
@app.post("/api/oci/test/{cid}")
|
@app.post("/api/oci/test/{cid}")
|
||||||
@@ -2354,6 +2441,7 @@ Se pedirem AWS, Azure, GCP ou outro provider, recuse educadamente.
|
|||||||
- Remote peering: APENAS UM LADO do par define `peer_id` e `peer_region_name`. O outro lado é criado SEM `peer_id`. Nunca gere dois RPCs apontando um para o outro (causa Cycle error). Exemplo: RPC_mad1 (sem peer_id) e RPC_mad3 (com peer_id = RPC_mad1.id, peer_region_name = var.region).
|
- Remote peering: APENAS UM LADO do par define `peer_id` e `peer_region_name`. O outro lado é criado SEM `peer_id`. Nunca gere dois RPCs apontando um para o outro (causa Cycle error). Exemplo: RPC_mad1 (sem peer_id) e RPC_mad3 (com peer_id = RPC_mad1.id, peer_region_name = var.region).
|
||||||
- Associação de route table com subnet deve ser via `route_table_id` na subnet ou `oci_core_route_table_attachment`.
|
- Associação de route table com subnet deve ser via `route_table_id` na subnet ou `oci_core_route_table_attachment`.
|
||||||
- `oci_network_firewall_network_firewall` exige `network_firewall_policy_id`.
|
- `oci_network_firewall_network_firewall` exige `network_firewall_policy_id`.
|
||||||
|
- DRG attachments: use `oci_core_drg_attachment` para VCN attachments (tipo padrão). Use `oci_core_drg_attachment_management` SOMENTE para REMOTE_PEERING_CONNECTION (único tipo suportado para management). Nunca duplique resource type+name entre arquivos (ex: dois `oci_core_drg_attachment.vcn_app_x`). Sempre associe `drg_route_table_id` nos VCN attachments para garantir roteamento correto.
|
||||||
|
|
||||||
### Proibições absolutas — módulos e variáveis
|
### Proibições absolutas — módulos e variáveis
|
||||||
- NUNCA use `module` blocks. Este workspace é flat (um único diretório). Não existem subdiretórios `modules/`. Toda a infraestrutura deve ser definida diretamente com `resource` e `data` blocks.
|
- NUNCA use `module` blocks. Este workspace é flat (um único diretório). Não existem subdiretórios `modules/`. Toda a infraestrutura deve ser definida diretamente com `resource` e `data` blocks.
|
||||||
@@ -2426,9 +2514,9 @@ def _validate_tf_resource_types(tf_code: str) -> list:
|
|||||||
Returns list of dicts with invalid types and suggestions."""
|
Returns list of dicts with invalid types and suggestions."""
|
||||||
import re as _re
|
import re as _re
|
||||||
from difflib import get_close_matches
|
from difflib import get_close_matches
|
||||||
# Extract all resource and data type declarations
|
# Extract only resource type declarations (data sources are read-only and may not be in the schema)
|
||||||
used_types = set()
|
used_types = set()
|
||||||
for m in _re.finditer(r'(?:resource|data)\s+"(\w+)"', tf_code):
|
for m in _re.finditer(r'resource\s+"(\w+)"', tf_code):
|
||||||
used_types.add(m.group(1))
|
used_types.add(m.group(1))
|
||||||
if not used_types:
|
if not used_types:
|
||||||
return []
|
return []
|
||||||
@@ -3549,7 +3637,7 @@ async def embed_upload_url(
|
|||||||
if not url.startswith(("http://", "https://")):
|
if not url.startswith(("http://", "https://")):
|
||||||
raise HTTPException(400, "URL inválida — deve começar com http:// ou https://")
|
raise HTTPException(400, "URL inválida — deve começar com http:// ou https://")
|
||||||
try:
|
try:
|
||||||
resp = req.get(url, timeout=30, headers={"User-Agent": "Mozilla/5.0 OCI-CIS-Agent/1.0"})
|
resp = req.get(url, timeout=30, headers={"User-Agent": "Mozilla/5.0 AI-Agent/1.0"})
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(400, f"Erro ao acessar URL: {str(e)[:300]}")
|
raise HTTPException(400, f"Erro ao acessar URL: {str(e)[:300]}")
|
||||||
@@ -4483,7 +4571,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 v" + VERSION + "**. Sem modelo GenAI configurado, uso respostas locais.\n\n"
|
return ("Sou o **AI Agent v" + VERSION + "** — Infrastructure & Security Engineer. 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.get("/api/chat/sessions")
|
@app.get("/api/chat/sessions")
|
||||||
@@ -4835,7 +4923,7 @@ class TfPromptReq(BaseModel):
|
|||||||
session_id: Optional[str] = None
|
session_id: Optional[str] = None
|
||||||
|
|
||||||
@app.post("/api/terraform/generate-prompt")
|
@app.post("/api/terraform/generate-prompt")
|
||||||
async def tf_generate_prompt(req: TfPromptReq, u=Depends(current_user)):
|
async def tf_generate_prompt(req: TfPromptReq, bg: BackgroundTasks, u=Depends(current_user)):
|
||||||
gc = None
|
gc = None
|
||||||
if req.genai_config:
|
if req.genai_config:
|
||||||
if req.genai_config.get("genai_config_id"):
|
if req.genai_config.get("genai_config_id"):
|
||||||
@@ -4911,16 +4999,23 @@ async def tf_generate_prompt(req: TfPromptReq, u=Depends(current_user)):
|
|||||||
if req.history:
|
if req.history:
|
||||||
hist = [{"role": "user" if m.get("role","").upper() in ("USER","user") else "assistant",
|
hist = [{"role": "user" if m.get("role","").upper() in ("USER","user") else "assistant",
|
||||||
"content": m.get("content", "")} for m in req.history]
|
"content": m.get("content", "")} for m in req.history]
|
||||||
try:
|
# Async background processing — same pattern as Chat/Terraform agents
|
||||||
answer, _, _ = _call_genai(gc, req.message, history=hist)
|
mid = str(uuid.uuid4())
|
||||||
# Save assistant response
|
|
||||||
with db() as c:
|
with db() as c:
|
||||||
c.execute("INSERT INTO chat_messages (id,session_id,user_id,role,content,model_id,status) VALUES (?,?,?,?,?,?,?)",
|
c.execute("INSERT INTO chat_messages (id,session_id,user_id,role,content,model_id,status) VALUES (?,?,?,?,?,?,?)",
|
||||||
(str(uuid.uuid4()), sid, u["id"], "assistant", answer, gc.get("model_id"), "done"))
|
(mid, sid, u["id"], "assistant", "", gc.get("model_id"), "processing"))
|
||||||
return {"prompt": answer, "session_id": sid}
|
def _tfp_background():
|
||||||
|
try:
|
||||||
|
answer, _, _ = _call_genai(gc, req.message, history=hist)
|
||||||
|
with db() as c:
|
||||||
|
c.execute("UPDATE chat_messages SET content=?, status='done' WHERE id=?", (answer, mid))
|
||||||
|
log.info(f"TF Prompt Generator completed: mid={mid} len={len(answer)}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"tf_generate_prompt error: {e}")
|
log.error(f"tf_generate_prompt error: {e}")
|
||||||
raise HTTPException(500, str(e)[:500])
|
with db() as c:
|
||||||
|
c.execute("UPDATE chat_messages SET content=?, status='failed' WHERE id=?", (str(e)[:500], mid))
|
||||||
|
bg.add_task(_tfp_background)
|
||||||
|
return {"message_id": mid, "session_id": sid, "status": "processing"}
|
||||||
|
|
||||||
@app.post("/api/terraform/chat")
|
@app.post("/api/terraform/chat")
|
||||||
async def terraform_chat(msg: ChatMsg, bg: BackgroundTasks, u=Depends(current_user)):
|
async def terraform_chat(msg: ChatMsg, bg: BackgroundTasks, u=Depends(current_user)):
|
||||||
@@ -4953,7 +5048,12 @@ async def tf_create_workspace(req: TfWorkspaceReq, u=Depends(current_user)):
|
|||||||
async def tf_list_workspaces(u=Depends(current_user)):
|
async def tf_list_workspaces(u=Depends(current_user)):
|
||||||
with db() as c:
|
with db() as c:
|
||||||
rows = c.execute(
|
rows = c.execute(
|
||||||
"SELECT id,name,session_id,oci_config_id,status,created_at,updated_at FROM terraform_workspaces WHERE user_id=? ORDER BY created_at DESC",
|
"""SELECT tw.id,tw.name,tw.session_id,tw.oci_config_id,tw.compartment_id,tw.status,
|
||||||
|
tw.plan_output,tw.apply_output,tw.destroy_output,tw.error,tw.created_at,tw.updated_at,
|
||||||
|
cs.title as session_title
|
||||||
|
FROM terraform_workspaces tw
|
||||||
|
INNER JOIN chat_sessions cs ON cs.id = tw.session_id AND cs.user_id = tw.user_id
|
||||||
|
WHERE tw.user_id=? ORDER BY tw.updated_at DESC""",
|
||||||
(u["id"],)).fetchall()
|
(u["id"],)).fetchall()
|
||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
@@ -5253,7 +5353,7 @@ def _write_tf_files(wdir: Path, tf_code: str):
|
|||||||
# Remove monolithic files that are fully duplicated
|
# Remove monolithic files that are fully duplicated
|
||||||
for fname in dupes_in:
|
for fname in dupes_in:
|
||||||
if all(
|
if all(
|
||||||
any(f2 != fname and f2 in [fl for fl in resource_locations.get(key, []) if fl != fname])
|
any(f2 != fname for f2 in resource_locations.get(key, []))
|
||||||
for key, flist in resource_locations.items()
|
for key, flist in resource_locations.items()
|
||||||
if fname in flist
|
if fname in flist
|
||||||
) and len(files) - 1 >= 2:
|
) and len(files) - 1 >= 2:
|
||||||
@@ -5881,7 +5981,7 @@ async def startup():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(f"Could not populate tf_valid_types: {e}")
|
log.warning(f"Could not populate tf_valid_types: {e}")
|
||||||
|
|
||||||
log.info(f"OCI CIS AI Agent v{VERSION} API started")
|
log.info(f"AI Agent v{VERSION} API started")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>OCI CIS AI Agent</title>
|
<title>AI Agent - Infrastructure & Security Engineer</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
:root,html.light {
|
:root,html.light {
|
||||||
@@ -374,6 +374,8 @@ html .spinner,html [class*="kpi-bar-fill"],html canvas{transition:none!important
|
|||||||
.exp-cats{flex-wrap:wrap}
|
.exp-cats{flex-wrap:wrap}
|
||||||
.exp-cat{padding:.3rem .5rem;font-size:.6rem}
|
.exp-cat{padding:.3rem .5rem;font-size:.6rem}
|
||||||
}
|
}
|
||||||
|
.retry-btn{display:inline-flex;align-items:center;gap:.25rem;margin-top:.35rem;padding:.25rem .65rem;font-size:.64rem;font-family:inherit;border-radius:6px;border:1px solid var(--rd);background:transparent;color:var(--rd);cursor:pointer;transition:all .15s}
|
||||||
|
.retry-btn:hover{background:var(--rd);color:#fff}
|
||||||
/* ── Terraform Agent ── */
|
/* ── Terraform Agent ── */
|
||||||
.tf-wrap{display:flex;flex-direction:row;height:calc(100vh - 56px - 3rem);overflow:hidden}
|
.tf-wrap{display:flex;flex-direction:row;height:calc(100vh - 56px - 3rem);overflow:hidden}
|
||||||
.tf-left{flex:6;display:flex;flex-direction:column;overflow:hidden;min-width:0}
|
.tf-left{flex:6;display:flex;flex-direction:column;overflow:hidden;min-width:0}
|
||||||
@@ -454,6 +456,33 @@ html .spinner,html [class*="kpi-bar-fill"],html canvas{transition:none!important
|
|||||||
.tf-prompt-saved{font-size:.62rem;color:var(--t3);transition:opacity .3s}
|
.tf-prompt-saved{font-size:.62rem;color:var(--t3);transition:opacity .3s}
|
||||||
.tf-prompt-name{background:var(--bg2);color:var(--t1);border:1px solid var(--bd);border-radius:4px;padding:.25rem .4rem;font-size:.72rem;width:100%;margin-bottom:.5rem;box-sizing:border-box}
|
.tf-prompt-name{background:var(--bg2);color:var(--t1);border:1px solid var(--bd);border-radius:4px;padding:.25rem .4rem;font-size:.72rem;width:100%;margin-bottom:.5rem;box-sizing:border-box}
|
||||||
.tf-modal h3{margin:0 0 .5rem;color:var(--rd)}
|
.tf-modal h3{margin:0 0 .5rem;color:var(--rd)}
|
||||||
|
/* ── Terraform Workspaces ── */
|
||||||
|
.tws-page{padding:1rem;height:calc(100vh - 56px - 3rem);overflow-y:auto}
|
||||||
|
.tws-hdr{display:flex;align-items:center;gap:.7rem;margin-bottom:1rem}
|
||||||
|
.tws-hdr h3{margin:0;font-size:.88rem;color:var(--t1)}
|
||||||
|
.tws-filter{display:flex;gap:.3rem}
|
||||||
|
.tws-filter-btn{padding:.2rem .55rem;font-size:.64rem;border-radius:12px;border:1px solid var(--bd);background:var(--bg);color:var(--t3);cursor:pointer;font-family:inherit;transition:all .15s}
|
||||||
|
.tws-filter-btn.on{background:var(--pp);color:#fff;border-color:var(--pp)}
|
||||||
|
.tws-group{margin-bottom:.7rem}
|
||||||
|
.tws-group-hdr{display:flex;align-items:center;gap:.5rem;padding:.4rem .6rem;background:var(--bg2);border:1px solid var(--bd);border-radius:8px;cursor:pointer;user-select:none;transition:background .12s}
|
||||||
|
.tws-group-hdr:hover{background:var(--bg3)}
|
||||||
|
.tws-group-arrow{width:14px;height:14px;transition:transform .2s;fill:var(--t3)}
|
||||||
|
.tws-group-hdr.open .tws-group-arrow{transform:rotate(90deg)}
|
||||||
|
.tws-group-date{font-size:.72rem;font-weight:700;color:var(--t1)}
|
||||||
|
.tws-group-cnt{font-size:.6rem;color:var(--t3);margin-left:auto}
|
||||||
|
.tws-group-body{padding:.3rem 0 0 0}
|
||||||
|
.tws-row{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;border:1px solid var(--bd);border-radius:8px;margin-bottom:.35rem;background:var(--bg);transition:border-color .15s}
|
||||||
|
.tws-row:hover{border-color:var(--pp)}
|
||||||
|
.tws-row-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:.15rem}
|
||||||
|
.tws-row-title{font-size:.74rem;font-weight:600;color:var(--t1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
|
.tws-row-sub{font-size:.6rem;color:var(--t3);display:flex;gap:.6rem;flex-wrap:wrap}
|
||||||
|
.tws-row-sub span{display:inline-flex;align-items:center;gap:.15rem}
|
||||||
|
.tws-row-acts{display:flex;gap:.3rem;align-items:center;flex-shrink:0}
|
||||||
|
.tws-row-acts .btn{font-size:.6rem;padding:2px 8px}
|
||||||
|
.tws-row-detail{margin:.3rem 0 .4rem .7rem;background:var(--bg2);border:1px solid var(--bd);border-radius:6px;padding:.5rem;max-height:200px;overflow:auto}
|
||||||
|
.tws-row-detail pre{font-size:.62rem;line-height:1.4;color:var(--t2);font-family:var(--fm);margin:0;white-space:pre-wrap;word-break:break-word}
|
||||||
|
.tws-empty{text-align:center;padding:3rem 1rem;color:var(--t3)}
|
||||||
|
.tws-empty p{margin:.3rem 0}
|
||||||
.tf-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--t3);font-size:.76rem;gap:.3rem}
|
.tf-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--t3);font-size:.76rem;gap:.3rem}
|
||||||
.tf-empty svg{width:40px;height:40px;fill:#7b42bc;opacity:.3}
|
.tf-empty svg{width:40px;height:40px;fill:#7b42bc;opacity:.3}
|
||||||
@media(max-width:768px){
|
@media(max-width:768px){
|
||||||
@@ -542,7 +571,9 @@ const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],g
|
|||||||
tfWs:null,tfWsList:[],tfPlanOut:'',tfApplyOut:'',tfDestroyOut:'',tfStatus:'draft',tfRunning:false,tfHadApply:false,tfConfirmDest:false,tfConfirmRollback:false,tfMdOpen:false,
|
tfWs:null,tfWsList:[],tfPlanOut:'',tfApplyOut:'',tfDestroyOut:'',tfStatus:'draft',tfRunning:false,tfHadApply:false,tfConfirmDest:false,tfConfirmRollback:false,tfMdOpen:false,
|
||||||
tfComps:[],tfCompLoading:false,tfFiles:[],tfBtab:'files',tfEditIdx:-1,tfResources:null,tfResLoading:false,tfRefStatus:'',tfBottomH:null,
|
tfComps:[],tfCompLoading:false,tfFiles:[],tfBtab:'files',tfEditIdx:-1,tfResources:null,tfResLoading:false,tfRefStatus:'',tfBottomH:null,
|
||||||
tfpMsgs:[],tfpLoading:false,tfpCopied:false,tfpModel:'',tfpMdOpen:false,tfpOci:'',tfpRegion:'',tfpCompartment:'',
|
tfpMsgs:[],tfpLoading:false,tfpCopied:false,tfpModel:'',tfpMdOpen:false,tfpOci:'',tfpRegion:'',tfpCompartment:'',
|
||||||
tfpHistOpen:false,tfpHistory:[],tfpSid:null};
|
tfpHistOpen:false,tfpHistory:[],tfpSid:null,
|
||||||
|
twsList:[],twsLoading:false,twsExpanded:null,twsFilter:'all',twsConfirmDel:null,twsConfirmDest:null,twsClosedGroups:null,
|
||||||
|
ociFormOpen:false,ociAuthType:'api_key'};
|
||||||
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;
|
||||||
@@ -603,13 +634,13 @@ function R(){try{
|
|||||||
}catch(e){console.error('Render error:',e);document.getElementById('app').innerHTML='<div style="padding:2rem;color:red"><h3>Render Error</h3><pre>'+e.message+'</pre></div>'}}
|
}catch(e){console.error('Render error:',e);document.getElementById('app').innerHTML='<div style="padding:2rem;color:red"><h3>Render Error</h3><pre>'+e.message+'</pre></div>'}}
|
||||||
function switchTab(t){S.tab=t;R();
|
function switchTab(t){S.tab=t;R();
|
||||||
const pg=document.getElementById('pg');if(pg){const pc=pg.querySelector('.pc');if(pc){pc.classList.add('tab-enter');setTimeout(()=>pc.classList.remove('tab-enter'),500)}}
|
const pg=document.getElementById('pg');if(pg){const pc=pg.querySelector('.pc');if(pc){pc.classList.add('tab-enter');setTimeout(()=>pc.classList.remove('tab-enter'),500)}}
|
||||||
if(t==='chat')loadHistory('chat');if(t==='terraform')loadHistory('terraform');if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl();if(t==='explorer'&&S.ociCfg.length&&!S.expTree.length){if(!S.expCfg)S.expCfg=S.ociCfg[0].id;expLoadTree();expLoadRegions()}
|
if(t==='chat')loadHistory('chat');if(t==='terraform')loadHistory('terraform');if(t==='tf-workspaces')tfWsLoadList();if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl();if(t==='explorer'&&S.ociCfg.length&&!S.expTree.length){if(!S.expCfg)S.expCfg=S.ociCfg[0].id;expLoadTree();expLoadRegions()}
|
||||||
const tm={'oci-config':'oci','genai':'genai','adb':'adb','mcp':'mcp'};if(tm[t])setTimeout(()=>refreshCLogs(tm[t]),100)}
|
const tm={'oci-config':'oci','genai':'genai','adb':'adb','mcp':'mcp'};if(tm[t])setTimeout(()=>refreshCLogs(tm[t]),100)}
|
||||||
|
|
||||||
/* ── Login ── */
|
/* ── Login ── */
|
||||||
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.5rem">${LOGO_R}
|
<div style="text-align:center;margin-bottom:1.5rem">${LOGO_R}
|
||||||
<h2>OCI CIS Agent</h2><div class="st">Oracle Cloud Infrastructure · Security</div></div>
|
<h2>AI Agent</h2><div class="st">Infrastructure & Security Engineer</div></div>
|
||||||
<div id="le"></div>
|
<div id="le"></div>
|
||||||
<div id="mfa-s" style="display:none"><div class="al al-i">Código MFA obrigatório</div>
|
<div id="mfa-s" style="display:none"><div class="al al-i">Código MFA obrigatório</div>
|
||||||
<div class="ig"><label>Código TOTP</label><input type="text" id="mfa-c" placeholder="000000" maxlength="6" style="text-align:center;font-size:1.1rem;letter-spacing:.3em;font-weight:600"></div>
|
<div class="ig"><label>Código TOTP</label><input type="text" id="mfa-c" placeholder="000000" maxlength="6" style="text-align:center;font-size:1.1rem;letter-spacing:.3em;font-weight:600"></div>
|
||||||
@@ -624,12 +655,12 @@ function rApp(){return`<div class="app">${rSb()}<div class="mc">${rTb()}<div cla
|
|||||||
|
|
||||||
const TF_IC='<svg viewBox="0 0 16 16" width="14" height="14" style="vertical-align:-2px;margin-right:2px"><path d="M5.6 1v4.2l3.6 2.1V3.1L5.6 1zm4.4 2.1v4.2l3.6-2.1V1L10 3.1zM1.4 3.5v4.2l3.6 2.1V5.6L1.4 3.5zM5.6 8.1v4.2L9.2 14.4V10.2L5.6 8.1z" fill="#7b42bc"/></svg>';
|
const TF_IC='<svg viewBox="0 0 16 16" width="14" height="14" style="vertical-align:-2px;margin-right:2px"><path d="M5.6 1v4.2l3.6 2.1V3.1L5.6 1zm4.4 2.1v4.2l3.6-2.1V1L10 3.1zM1.4 3.5v4.2l3.6 2.1V5.6L1.4 3.5zM5.6 8.1v4.2L9.2 14.4V10.2L5.6 8.1z" fill="#7b42bc"/></svg>';
|
||||||
function rSb(){
|
function rSb(){
|
||||||
const tabs=[['chat',IC.chat,'Chat Agent'],['terraform',TF_IC,'Terraform'],['tf-prompt',IC.edit,'Prompt Generator','sub'],['explorer',IC.search,'OCI Explorer'],['report',IC.chart,'Reports'],['downloads',IC.folder,'Downloads']];
|
const tabs=[['chat',IC.chat,'Chat Agent'],['terraform',TF_IC,'Terraform'],['tf-prompt',IC.edit,'Prompt Generator','sub'],['tf-workspaces',IC.folder,'Workspaces','sub'],['explorer',IC.search,'OCI Explorer'],['report',IC.chart,'Reports'],['downloads',IC.folder,'Downloads']];
|
||||||
const ctabs=[['oci-config',IC.cloud,'Credenciais OCI'],['genai',IC.brain,'GenAI Config'],['mcp',IC.plug,'MCP Servers'],['adb',IC.db,'ADB Vector'],['embeddings',IC.dna,'Embeddings'],['emb-consult',IC.search,'Consultar Embeddings','sub']];
|
const ctabs=[['oci-config',IC.cloud,'Credenciais OCI'],['genai',IC.brain,'GenAI Config'],['mcp',IC.plug,'MCP Servers'],['adb',IC.db,'ADB Vector'],['embeddings',IC.dna,'Embeddings'],['emb-consult',IC.search,'Consultar Embeddings','sub']];
|
||||||
const atabs=[['users',IC.users,'Usuários'],['mfa',IC.lock,'MFA'],['audit',IC.log,'Audit Log']];
|
const atabs=[['users',IC.users,'Usuários'],['mfa',IC.lock,'MFA'],['audit',IC.log,'Audit Log']];
|
||||||
const i=(S.user?.first_name||S.user?.username||'?')[0].toUpperCase();
|
const i=(S.user?.first_name||S.user?.username||'?')[0].toUpperCase();
|
||||||
return`<div class="sb">
|
return`<div class="sb">
|
||||||
<div class="sb-h"><h1>${LOGO_W} OCI CIS Agent</h1><div class="st">Infrastructure & Security · v${V}</div></div>
|
<div class="sb-h"><h1>${LOGO_W} AI Agent</h1><div class="st">Infrastructure & Security Engineer · 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]}')" ${t[3]==='sub'?'style="padding-left:2.2rem;font-size:.78rem;opacity:.9"':''}><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]}')" ${t[3]==='sub'?'style="padding-left:2.2rem;font-size:.78rem;opacity:.9"':''}><span class="ic">${t[1]}</span>${t[2]}</div>`).join('')}
|
||||||
<div class="nl">Configuração</div>
|
<div class="nl">Configuração</div>
|
||||||
@@ -640,15 +671,15 @@ ${atabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[
|
|||||||
<div class="lo-btn" onclick="toggleTheme()" title="Alternar tema">${document.documentElement.classList.contains('dark')?IC.sun:IC.moon}</div>
|
<div class="lo-btn" onclick="toggleTheme()" title="Alternar tema">${document.documentElement.classList.contains('dark')?IC.sun:IC.moon}</div>
|
||||||
<div class="lo-btn" onclick="doLogout()" title="Sair">⏻</div></div></div></div>`}
|
<div class="lo-btn" onclick="doLogout()" title="Sair">⏻</div></div></div></div>`}
|
||||||
|
|
||||||
function rTb(){const t={'chat':IC.chat+' AI Agent Chat','terraform':TF_IC+' Terraform Agent','tf-prompt':IC.edit+' Prompt Generator','explorer':IC.search+' OCI Account Explorer','report':IC.chart+' Compliance Reports','downloads':IC.folder+' Downloads','oci-config':IC.cloud+' Credenciais OCI','genai':IC.brain+' OCI Generative AI','mcp':IC.plug+' MCP Servers','adb':IC.db+' Autonomous DB Vector','embeddings':IC.dna+' Embeddings','emb-consult':IC.search+' Consultar Embeddings','users':IC.users+' Gerenciar Usuários','mfa':IC.lock+' Autenticação MFA','audit':IC.log+' Audit Log'};
|
function rTb(){const t={'chat':IC.chat+' AI Agent Chat','terraform':TF_IC+' Terraform Agent','tf-prompt':IC.edit+' Prompt Generator','tf-workspaces':IC.folder+' Terraform Workspaces','explorer':IC.search+' OCI Account Explorer','report':IC.chart+' Compliance Reports','downloads':IC.folder+' Downloads','oci-config':IC.cloud+' Credenciais OCI','genai':IC.brain+' OCI Generative AI','mcp':IC.plug+' MCP Servers','adb':IC.db+' Autonomous DB Vector','embeddings':IC.dna+' Embeddings','emb-consult':IC.search+' Consultar Embeddings','users':IC.users+' Gerenciar Usuários','mfa':IC.lock+' Autenticação MFA','audit':IC.log+' Audit Log'};
|
||||||
return`<div class="tb"><div class="tb-t">${t[S.tab]||''}</div><div class="tb-a"><span class="tag">v${V}</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'terraform':return rTerraform();case'tf-prompt':return rTfPrompt();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'embeddings':return rEmbeddings();case'emb-consult':return rEmbConsultPage();case'users':return rUsers();case'mfa':return rMfa();case'audit':return rAudit();default:return''}}
|
function rPg(){switch(S.tab){case'chat':return rChat();case'terraform':return rTerraform();case'tf-prompt':return rTfPrompt();case'tf-workspaces':return rTfWorkspaces();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'embeddings':return rEmbeddings();case'emb-consult':return rEmbConsultPage();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">${IC.bot}</div><p>Inicie uma conversa com o agente.</p><p style="font-size:.74rem;margin-top:.35rem">Selecione um modelo para começar.</p></div>`
|
const ms=S.msgs.length===0?`<div class="emp"><div class="eic">${IC.bot}</div><p>Inicie uma conversa com o agente.</p><p style="font-size:.74rem;margin-top:.35rem">Selecione um modelo para começar.</p></div>`
|
||||||
:S.msgs.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}</div>${m.t?`<div class="cm-ts">${m.t}</div>`:''}</div>`).join('');
|
:S.msgs.map((m,i)=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}</div>${m._failed?`<button class="retry-btn" onclick="chatRetry(${i})">↻ Reenviar</button>`:''}${m.t?`<div class="cm-ts">${m.t}</div>`:''}</div>`).join('');
|
||||||
const provs={};for(const[mid,info]of Object.entries(S.models)){const p=info.provider||'other';if(!provs[p])provs[p]=[];provs[p].push({id:mid,name:info.name})}
|
const provs={};for(const[mid,info]of Object.entries(S.models)){const p=info.provider||'other';if(!provs[p])provs[p]=[];provs[p].push({id:mid,name:info.name})}
|
||||||
const provOrder=['openai','google','meta','xai'];
|
const provOrder=['openai','google','meta','xai'];
|
||||||
let ddItems='';
|
let ddItems='';
|
||||||
@@ -802,19 +833,38 @@ async function sChat(){const el=document.getElementById('chi');const m=el.value.
|
|||||||
let resp=d.response;
|
let resp=d.response;
|
||||||
if(d.tools_used&&d.tools_used.length){resp+='\n\n'+IC.wrench+' **Tools utilizadas:** '+d.tools_used.map(t=>t.name).join(', ')}
|
if(d.tools_used&&d.tools_used.length){resp+='\n\n'+IC.wrench+' **Tools utilizadas:** '+d.tools_used.map(t=>t.name).join(', ')}
|
||||||
S.msgs.push({r:'assistant',c:resp,t:tstamp()});R();scCh()}}
|
S.msgs.push({r:'assistant',c:resp,t:tstamp()});R();scCh()}}
|
||||||
catch(e){S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:IC.err+' Erro: '+e.message,t:tstamp()});R()}
|
catch(e){S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:IC.err+' Erro: '+e.message,t:tstamp(),_failed:true});R()}
|
||||||
finally{el.disabled=false;el.focus();btns.forEach(b=>{b.disabled=false});if(sendBtn)sendBtn.textContent=sendBtn.dataset.origText||'Enviar →'}}
|
finally{el.disabled=false;el.focus();btns.forEach(b=>{b.disabled=false});if(sendBtn)sendBtn.textContent=sendBtn.dataset.origText||'Enviar →'}}
|
||||||
async function pollChatResult(mid){
|
async function pollChatResult(mid){
|
||||||
for(let i=0;i<1200;i++){
|
for(let i=0;i<1200;i++){
|
||||||
await new Promise(r=>setTimeout(r,i<10?1000:3000));
|
await new Promise(r=>setTimeout(r,i<10?1000:3000));
|
||||||
try{const r=await $api('/chat/'+mid+'/status');
|
try{const r=await $api('/chat/'+mid+'/status');
|
||||||
if(r.status==='done'){S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:r.content,t:tstamp()});R();scCh();return}
|
if(r.status==='done'){S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:r.content,t:tstamp()});R();scCh();return}
|
||||||
if(r.status==='failed'){S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:IC.err+' '+r.content,t:tstamp()});R();scCh();return}
|
if(r.status==='failed'){S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:IC.err+' '+r.content,t:tstamp(),_failed:true});R();scCh();return}
|
||||||
}catch(e){}}
|
}catch(e){}}
|
||||||
S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:IC.timeout+' Timeout: a resposta está demorando muito. Tente novamente.',t:tstamp()});R()}
|
S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:IC.timeout+' Timeout: a resposta está demorando muito. Tente novamente.',t:tstamp(),_failed:true});R()}
|
||||||
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()}
|
||||||
function newChat(){S.msgs=[];S.sid=null;S.chatFiles=[];R()}
|
function newChat(){S.msgs=[];S.sid=null;S.chatFiles=[];R()}
|
||||||
|
function chatRetry(idx){
|
||||||
|
// Find the last user message before this failed assistant message
|
||||||
|
let userMsg='';
|
||||||
|
for(let i=idx-1;i>=0;i--){if(S.msgs[i].r==='user'){userMsg=S.msgs[i].c;break}}
|
||||||
|
if(!userMsg)return;
|
||||||
|
// Remove the failed message
|
||||||
|
S.msgs.splice(idx,1);R();
|
||||||
|
// Re-send
|
||||||
|
const el=document.getElementById('chi');if(el)el.value=userMsg;
|
||||||
|
sChat();
|
||||||
|
}
|
||||||
|
function tfRetry(idx){
|
||||||
|
let userMsg='';
|
||||||
|
for(let i=idx-1;i>=0;i--){if(S.tfMsgs[i].r==='user'){userMsg=S.tfMsgs[i].c;break}}
|
||||||
|
if(!userMsg)return;
|
||||||
|
S.tfMsgs.splice(idx,1);R();
|
||||||
|
const el=document.getElementById('tfi');if(el)el.value=userMsg;
|
||||||
|
tfSend();
|
||||||
|
}
|
||||||
function newTfChat(){S.tfMsgs=[];S.tfSid=null;S.tfPlan=[];S.tfCode='';S.tfFiles=[];S.tfWs=null;S.tfPlanOut='';S.tfApplyOut='';S.tfDestroyOut='';S.tfStatus='draft';S.tfRunning=false;S.tfHadApply=false;S.tfConfirmDest=false;S.tfConfirmRollback=false;S.tfEditIdx=-1;R()}
|
function newTfChat(){S.tfMsgs=[];S.tfSid=null;S.tfPlan=[];S.tfCode='';S.tfFiles=[];S.tfWs=null;S.tfPlanOut='';S.tfApplyOut='';S.tfDestroyOut='';S.tfStatus='draft';S.tfRunning=false;S.tfHadApply=false;S.tfConfirmDest=false;S.tfConfirmRollback=false;S.tfEditIdx=-1;R()}
|
||||||
|
|
||||||
function tfToggleMenu(){
|
function tfToggleMenu(){
|
||||||
@@ -1207,7 +1257,7 @@ function rTfPrompt(){
|
|||||||
<div style="display:flex;flex-wrap:wrap;gap:.35rem;margin-top:.75rem;justify-content:center">${TFP_EXAMPLES.map(e=>`<button class="btn bs bsm" onclick="document.getElementById('tfpi').value='${e.replace(/'/g,"\\'")}';document.getElementById('tfpi').focus()" style="font-size:.66rem;white-space:normal;text-align:left;max-width:280px;line-height:1.35">${e}</button>`).join('')}</div></div>`
|
<div style="display:flex;flex-wrap:wrap;gap:.35rem;margin-top:.75rem;justify-content:center">${TFP_EXAMPLES.map(e=>`<button class="btn bs bsm" onclick="document.getElementById('tfpi').value='${e.replace(/'/g,"\\'")}';document.getElementById('tfpi').focus()" style="font-size:.66rem;white-space:normal;text-align:left;max-width:280px;line-height:1.35">${e}</button>`).join('')}</div></div>`
|
||||||
:msgs.map((m,i)=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}${m._raw?`
|
:msgs.map((m,i)=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}${m._raw?`
|
||||||
<div class="tfp-acts"><button class="tfp-act" onclick="tfpCopy(${i})">${m._copied?IC.ok+' Copiado':IC.clip+' Copiar'}</button>
|
<div class="tfp-acts"><button class="tfp-act" onclick="tfpCopy(${i})">${m._copied?IC.ok+' Copiado':IC.clip+' Copiar'}</button>
|
||||||
<button class="tfp-act tfp-act-tf" onclick="tfpSendToTf(${i})">${TF_IC} Terraform</button></div>`:''}</div></div>`).join('');
|
<button class="tfp-act tfp-act-tf" onclick="tfpSendToTf(${i})">${TF_IC} Terraform</button></div>`:''}</div>${m._failed?`<button class="retry-btn" onclick="tfpRetry(${i})">↻ Reenviar</button>`:''}</div>`).join('');
|
||||||
// Model dropdown — only recommended models, grouped by provider
|
// Model dropdown — only recommended models, grouped by provider
|
||||||
const tfpAllowed=['openai.gpt-4.1','openai.o3','openai.o4-mini','openai.gpt-5.1','openai.gpt-5.2','google.gemini-2.5-pro','google.gemini-2.5-flash'];
|
const tfpAllowed=['openai.gpt-4.1','openai.o3','openai.o4-mini','openai.gpt-5.1','openai.gpt-5.2','google.gemini-2.5-pro','google.gemini-2.5-flash'];
|
||||||
const tfpProvs={};tfpAllowed.forEach(mid=>{const mi=S.models[mid];if(mi){const p=mi.provider||'other';if(!tfpProvs[p])tfpProvs[p]=[];tfpProvs[p].push({id:mid,name:mi.name})}});
|
const tfpProvs={};tfpAllowed.forEach(mid=>{const mi=S.models[mid];if(mi){const p=mi.provider||'other';if(!tfpProvs[p])tfpProvs[p]=[];tfpProvs[p].push({id:mid,name:mi.name})}});
|
||||||
@@ -1257,10 +1307,26 @@ async function tfpSend(){
|
|||||||
if(S.tfpSid)body.session_id=S.tfpSid;
|
if(S.tfpSid)body.session_id=S.tfpSid;
|
||||||
const d=await $api('/terraform/generate-prompt',{method:'POST',body});
|
const d=await $api('/terraform/generate-prompt',{method:'POST',body});
|
||||||
if(d.session_id)S.tfpSid=d.session_id;
|
if(d.session_id)S.tfpSid=d.session_id;
|
||||||
S.tfpMsgs.push({r:'assistant',c:d.prompt,_raw:d.prompt});
|
if(d.status==='processing'&&d.message_id){await tfpPollResult(d.message_id)}
|
||||||
}catch(e){S.tfpMsgs.push({r:'assistant',c:IC.err+' Erro: '+e.message})}
|
else if(d.prompt){S.tfpMsgs.push({r:'assistant',c:d.prompt,_raw:d.prompt});S.tfpLoading=false;R()}
|
||||||
S.tfpLoading=false;R();
|
}catch(e){S.tfpMsgs.push({r:'assistant',c:IC.err+' Erro: '+e.message,_failed:true});S.tfpLoading=false;R()}}
|
||||||
const mc2=document.getElementById('tfpchm');if(mc2)mc2.scrollTop=mc2.scrollHeight}
|
async function tfpPollResult(mid){
|
||||||
|
for(let i=0;i<600;i++){
|
||||||
|
await new Promise(r=>setTimeout(r,i<10?1000:3000));
|
||||||
|
try{const r=await $api('/chat/'+mid+'/status');
|
||||||
|
if(r.status==='done'){S.tfpMsgs.push({r:'assistant',c:r.content,_raw:r.content});S.tfpLoading=false;R();
|
||||||
|
const mc=document.getElementById('tfpchm');if(mc)mc.scrollTop=mc.scrollHeight;return}
|
||||||
|
if(r.status==='failed'){S.tfpMsgs.push({r:'assistant',c:IC.err+' '+r.content,_failed:true});S.tfpLoading=false;R();return}
|
||||||
|
}catch(e){}}
|
||||||
|
S.tfpMsgs.push({r:'assistant',c:IC.timeout+' Timeout: a geração está demorando muito.',_failed:true});S.tfpLoading=false;R()}
|
||||||
|
function tfpRetry(idx){
|
||||||
|
let userMsg='';
|
||||||
|
for(let i=idx-1;i>=0;i--){if(S.tfpMsgs[i].r==='user'){userMsg=S.tfpMsgs[i].c;break}}
|
||||||
|
if(!userMsg)return;
|
||||||
|
S.tfpMsgs.splice(idx,1);R();
|
||||||
|
const el=document.getElementById('tfpi');if(el)el.value=userMsg;
|
||||||
|
tfpSend();
|
||||||
|
}
|
||||||
function tfpPickModel(v){S.tfpModel=v;S.tfpMdOpen=false;
|
function tfpPickModel(v){S.tfpModel=v;S.tfpMdOpen=false;
|
||||||
if(v&&!v.startsWith('cfg:')&&!S.tfpOci&&S.ociCfg.length){S.tfpOci=S.ociCfg[0].id;S.tfpRegion=S.ociCfg[0].region;S.tfpCompartment=S.ociCfg[0].compartment_id||''}
|
if(v&&!v.startsWith('cfg:')&&!S.tfpOci&&S.ociCfg.length){S.tfpOci=S.ociCfg[0].id;S.tfpRegion=S.ociCfg[0].region;S.tfpCompartment=S.ociCfg[0].compartment_id||''}
|
||||||
else if(v&&v.startsWith('cfg:')){const g=S.genaiCfg.find(x=>x.id===v.slice(4));if(g&&g.oci_config_id&&!S.tfpOci){S.tfpOci=g.oci_config_id;const c=S.ociCfg.find(x=>x.id===g.oci_config_id);if(c){S.tfpRegion=c.region;S.tfpCompartment=c.compartment_id||''}}}
|
else if(v&&v.startsWith('cfg:')){const g=S.genaiCfg.find(x=>x.id===v.slice(4));if(g&&g.oci_config_id&&!S.tfpOci){S.tfpOci=g.oci_config_id;const c=S.ociCfg.find(x=>x.id===g.oci_config_id);if(c){S.tfpRegion=c.region;S.tfpCompartment=c.compartment_id||''}}}
|
||||||
@@ -1292,7 +1358,7 @@ function tfpSendToTf(idx){
|
|||||||
function rTerraform(){
|
function rTerraform(){
|
||||||
const ms=S.tfMsgs.length===0
|
const ms=S.tfMsgs.length===0
|
||||||
?`<div class="tf-empty">${TF_ICON}<p>Descreva a infraestrutura OCI desejada</p><p style="font-size:.68rem;opacity:.6">Ex: "Crie uma VCN com subnets pública e privada, internet gateway e um compute instance"</p></div>`
|
?`<div class="tf-empty">${TF_ICON}<p>Descreva a infraestrutura OCI desejada</p><p style="font-size:.68rem;opacity:.6">Ex: "Crie uma VCN com subnets pública e privada, internet gateway e um compute instance"</p></div>`
|
||||||
:S.tfMsgs.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${m.r==='assistant'?fmTf(m.c):fm(m.c)}</div>${m.t?`<div class="cm-ts">${m.t}</div>`:''}</div>`).join('');
|
:S.tfMsgs.map((m,i)=>`<div class="cm cm-${m.r}"><div class="cb">${m.r==='assistant'?fmTf(m.c):fm(m.c)}</div>${m._failed?`<button class="retry-btn" onclick="tfRetry(${i})">↻ Reenviar</button>`:''}${m.t?`<div class="cm-ts">${m.t}</div>`:''}</div>`).join('');
|
||||||
|
|
||||||
// Model dropdown — same filtered models as Prompt Generator, grouped by provider
|
// Model dropdown — same filtered models as Prompt Generator, grouped by provider
|
||||||
const tfAllowed=['openai.gpt-4.1','openai.o3','openai.o4-mini','openai.gpt-5.1','openai.gpt-5.2','google.gemini-2.5-pro','google.gemini-2.5-flash'];
|
const tfAllowed=['openai.gpt-4.1','openai.o3','openai.o4-mini','openai.gpt-5.1','openai.gpt-5.2','google.gemini-2.5-pro','google.gemini-2.5-flash'];
|
||||||
@@ -1405,7 +1471,7 @@ function rTfActions(){
|
|||||||
const canReplan=S.tfCode&&['planned','failed','applied'].includes(st);
|
const canReplan=S.tfCode&&['planned','failed','applied'].includes(st);
|
||||||
if(canPlan&&!canReplan)btns+=`<button class="btn bsm" style="background:var(--pp);color:#fff;border-color:var(--pp);font-size:.72rem;padding:4px 12px;gap:4px;display:inline-flex;align-items:center" onclick="tfSaveAndPlan()">${TF_ICON} Plan</button>`;
|
if(canPlan&&!canReplan)btns+=`<button class="btn bsm" style="background:var(--pp);color:#fff;border-color:var(--pp);font-size:.72rem;padding:4px 12px;gap:4px;display:inline-flex;align-items:center" onclick="tfSaveAndPlan()">${TF_ICON} Plan</button>`;
|
||||||
if(canReplan)btns+=`<button class="btn bsm" style="background:var(--pp);color:#fff;border-color:var(--pp);font-size:.72rem;padding:4px 12px;gap:4px;display:inline-flex;align-items:center" onclick="tfSaveAndPlan()">${TF_ICON} Re-plan</button>`;
|
if(canReplan)btns+=`<button class="btn bsm" style="background:var(--pp);color:#fff;border-color:var(--pp);font-size:.72rem;padding:4px 12px;gap:4px;display:inline-flex;align-items:center" onclick="tfSaveAndPlan()">${TF_ICON} Re-plan</button>`;
|
||||||
if(st==='failed'&&S.tfPlanOut&&S.tfFiles.length&&S.tfModel)btns+=`<button class="btn bsm" style="background:#e67e22;color:#fff;border-color:#e67e22;font-size:.72rem;padding:4px 12px;gap:4px;display:inline-flex;align-items:center" onclick="tfAutoFix()">${IC.fix} Auto-corrigir</button>`;
|
if(st==='failed'&&S.tfPlanOut&&S.tfFiles.length)btns+=`<button class="btn bsm" style="background:#e67e22;color:#fff;border-color:#e67e22;font-size:.72rem;padding:4px 12px;gap:4px;display:inline-flex;align-items:center" onclick="tfAutoFix()">${IC.fix} Auto-corrigir</button>`;
|
||||||
if(st==='failed'&&(S.tfApplyOut||S.tfHadApply))btns+='<button class="btn bsm" style="background:var(--rd);color:#fff;border-color:var(--rd);font-size:.72rem;padding:4px 12px;gap:4px;display:inline-flex;align-items:center" onclick="S.tfConfirmRollback=true;R()">↩ Rollback</button>';
|
if(st==='failed'&&(S.tfApplyOut||S.tfHadApply))btns+='<button class="btn bsm" style="background:var(--rd);color:#fff;border-color:var(--rd);font-size:.72rem;padding:4px 12px;gap:4px;display:inline-flex;align-items:center" onclick="S.tfConfirmRollback=true;R()">↩ Rollback</button>';
|
||||||
if(st==='planned')btns+='<button class="btn bsm" style="background:var(--gn);color:#fff;border-color:var(--gn);font-size:.72rem;padding:4px 12px" onclick="tfApply()">✓ Apply</button>';
|
if(st==='planned')btns+='<button class="btn bsm" style="background:var(--gn);color:#fff;border-color:var(--gn);font-size:.72rem;padding:4px 12px" onclick="tfApply()">✓ Apply</button>';
|
||||||
if(st==='applied'||st==='planned')btns+='<button class="btn bsm" style="background:var(--rd);color:#fff;border-color:var(--rd);font-size:.72rem;padding:4px 12px" onclick="S.tfConfirmDest=true;R()">✕ Destroy</button>';
|
if(st==='applied'||st==='planned')btns+='<button class="btn bsm" style="background:var(--rd);color:#fff;border-color:var(--rd);font-size:.72rem;padding:4px 12px" onclick="S.tfConfirmDest=true;R()">✕ Destroy</button>';
|
||||||
@@ -1699,7 +1765,7 @@ async function tfSend(){
|
|||||||
if(S.tfHistOpen)loadHistory('terraform');
|
if(S.tfHistOpen)loadHistory('terraform');
|
||||||
if(d.status==='processing'&&d.message_id){await tfPollResult(d.message_id)}
|
if(d.status==='processing'&&d.message_id){await tfPollResult(d.message_id)}
|
||||||
else{S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:d.response||d.content||'Sem resposta',t:tstamp()});R();tfScroll()}
|
else{S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:d.response||d.content||'Sem resposta',t:tstamp()});R();tfScroll()}
|
||||||
}catch(e){S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:IC.err+' Erro: '+e.message,t:tstamp()});R()}
|
}catch(e){S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:IC.err+' Erro: '+e.message,t:tstamp(),_failed:true});R()}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tfPollResult(mid){
|
async function tfPollResult(mid){
|
||||||
@@ -1708,13 +1774,17 @@ async function tfPollResult(mid){
|
|||||||
try{const r=await $api('/chat/'+mid+'/status');
|
try{const r=await $api('/chat/'+mid+'/status');
|
||||||
if(r.status==='done'){S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:r.content,t:tstamp()});R();tfScroll();
|
if(r.status==='done'){S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:r.content,t:tstamp()});R();tfScroll();
|
||||||
if(r.content&&r.content.includes('```plan')){S.tfPanel='plan';R()}return}
|
if(r.content&&r.content.includes('```plan')){S.tfPanel='plan';R()}return}
|
||||||
if(r.status==='failed'){S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:IC.err+' '+r.content,t:tstamp()});R();tfScroll();return}
|
if(r.status==='failed'){S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:IC.err+' '+r.content,t:tstamp(),_failed:true});R();tfScroll();return}
|
||||||
}catch(e){}
|
}catch(e){}
|
||||||
}
|
}
|
||||||
S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:IC.timeout+' Timeout.',t:tstamp()});R()
|
S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:IC.timeout+' Timeout.',t:tstamp(),_failed:true});R()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tfSaveAndPlan(){
|
async function tfSaveAndPlan(){
|
||||||
|
// Build tfCode from files if empty but files exist
|
||||||
|
if(!S.tfCode&&S.tfFiles.length){
|
||||||
|
S.tfCode=S.tfFiles.map(f=>'// filename: '+f.name+'\n'+f.content).join('\n\n');
|
||||||
|
}
|
||||||
if(!S.tfCode){alert('Gere código Terraform primeiro.');return}
|
if(!S.tfCode){alert('Gere código Terraform primeiro.');return}
|
||||||
// Resolve OCI config
|
// Resolve OCI config
|
||||||
let ociId=S.tfOci;
|
let ociId=S.tfOci;
|
||||||
@@ -1733,7 +1803,7 @@ async function tfSaveAndPlan(){
|
|||||||
await $api('/terraform/workspaces/'+S.tfWs+'/plan',{method:'POST'});
|
await $api('/terraform/workspaces/'+S.tfWs+'/plan',{method:'POST'});
|
||||||
S.tfStatus='planning';S.tfRunning=true;S.tfPlanOut='';S.tfApplyOut='';S.tfDestroyOut='';S.tfBtab='plan';R();
|
S.tfStatus='planning';S.tfRunning=true;S.tfPlanOut='';S.tfApplyOut='';S.tfDestroyOut='';S.tfBtab='plan';R();
|
||||||
await tfPollExec();
|
await tfPollExec();
|
||||||
}catch(e){alert('Erro: '+e.message)}
|
}catch(e){alert('Erro ao executar plan: '+e.message)}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tfApply(){
|
async function tfApply(){
|
||||||
@@ -1764,7 +1834,8 @@ async function tfCancel(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function tfAutoFix(){
|
async function tfAutoFix(){
|
||||||
if(!S.tfPlanOut||!S.tfFiles.length||!S.tfModel)return;
|
if(!S.tfPlanOut||!S.tfFiles.length)return;
|
||||||
|
if(!S.tfModel){S.tfMsgs.push({r:'assistant',c:IC.warn+' Selecione um modelo antes de usar Auto-corrigir.',t:tstamp()});R();return}
|
||||||
// Extract errors from plan output
|
// Extract errors from plan output
|
||||||
const errLines=S.tfPlanOut.split('\n').filter(l=>/^Error:|^\s+on\s+\S+\.tf\s+line|^\s+\d+:|has not been declared|is not expected|Missing required|Unsupported|Invalid|Duplicate|undeclared/i.test(l));
|
const errLines=S.tfPlanOut.split('\n').filter(l=>/^Error:|^\s+on\s+\S+\.tf\s+line|^\s+\d+:|has not been declared|is not expected|Missing required|Unsupported|Invalid|Duplicate|undeclared/i.test(l));
|
||||||
const errCtx=errLines.length?errLines.join('\n'):S.tfPlanOut.slice(-2000);
|
const errCtx=errLines.length?errLines.join('\n'):S.tfPlanOut.slice(-2000);
|
||||||
@@ -1820,6 +1891,219 @@ async function tfPollExec(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ── Terraform Workspaces ── */
|
||||||
|
async function tfWsLoadList(){
|
||||||
|
S.twsLoading=true;R();
|
||||||
|
try{S.twsList=await $api('/terraform/workspaces')}catch(e){S.twsList=[]}
|
||||||
|
S.twsLoading=false;R();
|
||||||
|
}
|
||||||
|
function rTfWorkspaces(){
|
||||||
|
const f=S.twsFilter;
|
||||||
|
let items=S.twsList||[];
|
||||||
|
if(f==='active')items=items.filter(w=>['applied','planned','planning','applying'].includes(w.status));
|
||||||
|
else if(f==='destroyed')items=items.filter(w=>w.status==='destroyed');
|
||||||
|
else if(f==='failed')items=items.filter(w=>w.status==='failed');
|
||||||
|
else if(f==='draft')items=items.filter(w=>w.status==='draft');
|
||||||
|
const counts={all:(S.twsList||[]).length,active:(S.twsList||[]).filter(w=>['applied','planned','planning','applying'].includes(w.status)).length,destroyed:(S.twsList||[]).filter(w=>w.status==='destroyed').length,failed:(S.twsList||[]).filter(w=>w.status==='failed').length,draft:(S.twsList||[]).filter(w=>w.status==='draft').length};
|
||||||
|
const fb=(k,l)=>`<button class="tws-filter-btn${f===k?' on':''}" onclick="S.twsFilter='${k}';R()">${l} (${counts[k]})</button>`;
|
||||||
|
// Group by date
|
||||||
|
const groups={};
|
||||||
|
for(const w of items){
|
||||||
|
const dt=(w.updated_at||w.created_at||'').slice(0,10);
|
||||||
|
const label=_twsDateLabel(dt);
|
||||||
|
if(!groups[label])groups[label]={date:dt,items:[]};
|
||||||
|
groups[label].items.push(w);
|
||||||
|
}
|
||||||
|
const groupKeys=Object.keys(groups);
|
||||||
|
let body='';
|
||||||
|
if(groupKeys.length){
|
||||||
|
body=groupKeys.map(label=>{
|
||||||
|
const g=groups[label];
|
||||||
|
const open=S.twsClosedGroups?!S.twsClosedGroups.has(label):true;
|
||||||
|
return`<div class="tws-group">
|
||||||
|
<div class="tws-group-hdr${open?' open':''}" onclick="twsToggleGroup('${label.replace(/'/g,"\\'")}')">
|
||||||
|
<svg class="tws-group-arrow" viewBox="0 0 16 16"><path d="M6 3l5 5-5 5z"/></svg>
|
||||||
|
<span class="tws-group-date">${label}</span>
|
||||||
|
<span class="tws-group-cnt">${g.items.length} workspace${g.items.length>1?'s':''}</span>
|
||||||
|
</div>
|
||||||
|
${open?`<div class="tws-group-body">${g.items.map(w=>_rTwsRow(w)).join('')}</div>`:''}
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
}else{
|
||||||
|
body=S.twsLoading?'<div class="tws-empty"><p>Carregando workspaces...</p></div>'
|
||||||
|
:'<div class="tws-empty"><div style="font-size:2rem;margin-bottom:.5rem">${TF_IC}</div><p>Nenhum workspace encontrado</p><p style="font-size:.7rem">Crie workspaces pelo Terraform Agent</p></div>';
|
||||||
|
}
|
||||||
|
return`<div class="tws-page">
|
||||||
|
<div class="tws-hdr">
|
||||||
|
<h3>${TF_IC} Terraform Workspaces</h3>
|
||||||
|
<div style="flex:1"></div>
|
||||||
|
<div class="tws-filter">${fb('all','Todos')}${fb('active','Ativos')}${fb('draft','Draft')}${fb('failed','Falha')}${fb('destroyed','Destruídos')}</div>
|
||||||
|
<button class="btn bs bsm" onclick="tfWsLoadList()" style="font-size:.66rem;padding:3px 10px">${S.twsLoading?'Carregando...':'Atualizar'}</button>
|
||||||
|
</div>
|
||||||
|
${body}
|
||||||
|
${S.twsConfirmDest?_rTwsConfirmDestroy():''}
|
||||||
|
${S.twsConfirmDel?_rTwsConfirmDelete():''}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
function _twsDateLabel(dt){
|
||||||
|
if(!dt)return'Sem data';
|
||||||
|
const today=new Date().toISOString().slice(0,10);
|
||||||
|
const yesterday=new Date(Date.now()-86400000).toISOString().slice(0,10);
|
||||||
|
if(dt===today)return'Hoje';
|
||||||
|
if(dt===yesterday)return'Ontem';
|
||||||
|
const d=new Date(dt+'T00:00:00');
|
||||||
|
const diff=Math.floor((new Date()-d)/86400000);
|
||||||
|
if(diff<7)return d.toLocaleDateString('pt-BR',{weekday:'long'});
|
||||||
|
return d.toLocaleDateString('pt-BR',{day:'2-digit',month:'short',year:'numeric'});
|
||||||
|
}
|
||||||
|
function twsToggleGroup(label){
|
||||||
|
if(!S.twsClosedGroups)S.twsClosedGroups=new Set();
|
||||||
|
if(S.twsClosedGroups.has(label))S.twsClosedGroups.delete(label);else S.twsClosedGroups.add(label);
|
||||||
|
R();
|
||||||
|
}
|
||||||
|
function _rTwsRow(w){
|
||||||
|
const badge=_tfBadge(w.status);
|
||||||
|
const oc=S.ociCfg.find(x=>x.id===w.oci_config_id);
|
||||||
|
const ocName=oc?(oc.name||oc.region||w.oci_config_id):'—';
|
||||||
|
const exp=S.twsExpanded===w.id;
|
||||||
|
const ago=_twsTimeAgo(w.updated_at||w.created_at);
|
||||||
|
const hasOutput=w.plan_output||w.apply_output||w.destroy_output||w.error;
|
||||||
|
const title=w.session_title||w.name||'workspace';
|
||||||
|
let acts='';
|
||||||
|
const running=['planning','applying','destroying'].includes(w.status);
|
||||||
|
if(running){acts+=`<button class="btn bsm" style="background:var(--rd);color:#fff;border-color:var(--rd)" onclick="event.stopPropagation();twsCancel('${w.id}')">Cancelar</button>`}
|
||||||
|
else{
|
||||||
|
acts+=`<button class="btn bs bsm" onclick="event.stopPropagation();twsOpen('${w.id}')" title="Abrir no Terraform Agent">Abrir</button>`;
|
||||||
|
if(['draft','failed','destroyed','planned','applied'].includes(w.status))acts+=`<button class="btn bsm" style="background:var(--pp);color:#fff;border-color:var(--pp)" onclick="event.stopPropagation();twsPlan('${w.id}')">Plan</button>`;
|
||||||
|
if(w.status==='planned')acts+=`<button class="btn bsm" style="background:var(--gn);color:#fff;border-color:var(--gn)" onclick="event.stopPropagation();twsApply('${w.id}')">Apply</button>`;
|
||||||
|
if(w.status==='applied'||w.status==='planned')acts+=`<button class="btn bsm" style="background:var(--rd);color:#fff;border-color:var(--rd)" onclick="event.stopPropagation();S.twsConfirmDest='${w.id}';R()">Destroy</button>`;
|
||||||
|
acts+=`<button class="btn bsm" style="color:var(--rd);border-color:var(--rd);background:transparent" onclick="event.stopPropagation();S.twsConfirmDel='${w.id}';R()" title="Excluir">✕</button>`;
|
||||||
|
}
|
||||||
|
let detail='';
|
||||||
|
if(exp&&hasOutput){
|
||||||
|
const out=w.error?'ERROR: '+w.error:(w.destroy_output||w.apply_output||w.plan_output||'');
|
||||||
|
detail=`<div class="tws-row-detail"><pre>${escHtml(out)}</pre></div>`;
|
||||||
|
}
|
||||||
|
return`<div>
|
||||||
|
<div class="tws-row" onclick="${hasOutput&&!running?`S.twsExpanded=S.twsExpanded==='${w.id}'?null:'${w.id}';R()`:''}">
|
||||||
|
${badge}
|
||||||
|
<div class="tws-row-info">
|
||||||
|
<div class="tws-row-title" title="${escHtml(title)}">${escHtml(title)}</div>
|
||||||
|
<div class="tws-row-sub">
|
||||||
|
<span>${IC.cloud} ${escHtml(ocName)}</span>
|
||||||
|
${w.compartment_id?`<span title="${w.compartment_id}">...${w.compartment_id.slice(-8)}</span>`:''}
|
||||||
|
<span>${ago}</span>
|
||||||
|
${hasOutput&&!running?`<span style="color:var(--pp);cursor:pointer">${exp?'▾ ocultar':'▸ ver output'}</span>`:''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tws-row-acts">${acts}</div>
|
||||||
|
</div>
|
||||||
|
${detail}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
function _twsTimeAgo(ts){
|
||||||
|
if(!ts)return'—';
|
||||||
|
const d=new Date(ts+'Z');const now=new Date();const s=Math.floor((now-d)/1000);
|
||||||
|
if(s<60)return'agora';if(s<3600)return Math.floor(s/60)+'min';if(s<86400)return Math.floor(s/3600)+'h';return Math.floor(s/86400)+'d';
|
||||||
|
}
|
||||||
|
function _rTwsConfirmDestroy(){
|
||||||
|
return`<div class="tf-modal-overlay" onclick="S.twsConfirmDest=null;R()"><div class="tf-modal" onclick="event.stopPropagation()">
|
||||||
|
<h3>Confirmar Destroy</h3>
|
||||||
|
<p style="font-size:.76rem;color:var(--t2);margin:.5rem 0">Esta ação irá destruir todos os recursos provisionados por este workspace. Digite <b>DESTROY</b> para confirmar:</p>
|
||||||
|
<input type="text" id="twsDestInput" placeholder="DESTROY" style="width:100%;padding:.4rem;font-size:.8rem;text-align:center;border:1px solid var(--rd);border-radius:6px;background:var(--bg2);color:var(--t1);box-sizing:border-box">
|
||||||
|
<div style="display:flex;gap:.5rem;justify-content:flex-end;margin-top:.7rem">
|
||||||
|
<button class="btn bs" onclick="S.twsConfirmDest=null;R()">Cancelar</button>
|
||||||
|
<button class="btn" style="background:var(--rd);color:#fff;border-color:var(--rd)" onclick="twsDoDestroy()">Destroy</button>
|
||||||
|
</div>
|
||||||
|
</div></div>`;
|
||||||
|
}
|
||||||
|
function _rTwsConfirmDelete(){
|
||||||
|
return`<div class="tf-modal-overlay" onclick="S.twsConfirmDel=null;R()"><div class="tf-modal" onclick="event.stopPropagation()">
|
||||||
|
<h3>Excluir Workspace</h3>
|
||||||
|
<p style="font-size:.76rem;color:var(--t2);margin:.5rem 0">Deseja excluir este workspace permanentemente? Arquivos locais serão removidos.</p>
|
||||||
|
<div style="display:flex;gap:.5rem;justify-content:flex-end;margin-top:.7rem">
|
||||||
|
<button class="btn bs" onclick="S.twsConfirmDel=null;R()">Cancelar</button>
|
||||||
|
<button class="btn" style="background:var(--rd);color:#fff;border-color:var(--rd)" onclick="twsDoDelete()">Excluir</button>
|
||||||
|
</div>
|
||||||
|
</div></div>`;
|
||||||
|
}
|
||||||
|
async function twsOpen(wid){
|
||||||
|
const ws=S.twsList.find(w=>w.id===wid);if(!ws)return;
|
||||||
|
S.tab='terraform';
|
||||||
|
S.tfSid=ws.session_id;S.tfWs=wid;S.tfStatus=ws.status;
|
||||||
|
try{
|
||||||
|
const detail=await $api('/terraform/workspaces/'+wid);
|
||||||
|
if(detail.tf_code)S.tfCode=detail.tf_code;
|
||||||
|
if(detail.plan_output)S.tfPlanOut=detail.plan_output;
|
||||||
|
if(detail.apply_output)S.tfApplyOut=detail.apply_output;
|
||||||
|
if(detail.destroy_output)S.tfDestroyOut=detail.destroy_output;
|
||||||
|
S.tfOci=detail.oci_config_id||'';S.tfCompartment=detail.compartment_id||'';
|
||||||
|
if(detail.oci_config_id){
|
||||||
|
const oc=S.ociCfg.find(x=>x.id===detail.oci_config_id);
|
||||||
|
if(oc)S.tfRegion=oc.region;
|
||||||
|
}
|
||||||
|
if(detail.tf_code){
|
||||||
|
S.tfCode=detail.tf_code;
|
||||||
|
const parts=detail.tf_code.split(/^\/\/\s*filename:\s*/m).filter(Boolean);
|
||||||
|
if(parts.length>1){S.tfFiles=parts.map(p=>{const nl=p.indexOf('\n');const fn=p.substring(0,nl).trim();const code=p.substring(nl+1).trim();return{name:fn,content:code,size:code.length}})}
|
||||||
|
else{const split=_splitTfMonolith(detail.tf_code);S.tfFiles=Object.entries(split).map(([fn,code])=>({name:fn,content:code,size:code.length}))}
|
||||||
|
}
|
||||||
|
if(detail.status==='applied')S.tfHadApply=true;
|
||||||
|
if(S.tfPlanOut)S.tfBtab='plan';
|
||||||
|
else if(S.tfFiles.length)S.tfBtab='files';
|
||||||
|
if(S.tfOci){tfLoadComps();if(S.tfCompartment)tfLoadResources()}
|
||||||
|
}catch(e){}
|
||||||
|
try{const msgs=await $api('/chat/sessions/'+ws.session_id+'/messages');S.tfMsgs=msgs.map(m=>({r:m.role,c:m.content,t:m.created_at?new Date(m.created_at+'Z').toLocaleTimeString():''}))}catch(e){S.tfMsgs=[]}
|
||||||
|
R();
|
||||||
|
}
|
||||||
|
async function twsPlan(wid){
|
||||||
|
try{await $api('/terraform/workspaces/'+wid+'/plan',{method:'POST'})}catch(e){alert('Erro: '+e.message);return}
|
||||||
|
tfWsLoadList();_twsPollWs(wid);
|
||||||
|
}
|
||||||
|
async function twsApply(wid){
|
||||||
|
try{await $api('/terraform/workspaces/'+wid+'/apply',{method:'POST'})}catch(e){alert('Erro: '+e.message);return}
|
||||||
|
tfWsLoadList();_twsPollWs(wid);
|
||||||
|
}
|
||||||
|
async function twsDoDestroy(){
|
||||||
|
const wid=S.twsConfirmDest;if(!wid)return;
|
||||||
|
const v=document.getElementById('twsDestInput');if(!v||v.value!=='DESTROY'){v.style.borderColor='var(--rd)';return}
|
||||||
|
S.twsConfirmDest=null;
|
||||||
|
try{await $api('/terraform/workspaces/'+wid+'/destroy',{method:'POST',body:{confirm:'DESTROY'}})}catch(e){alert('Erro: '+e.message);R();return}
|
||||||
|
tfWsLoadList();_twsPollWs(wid);
|
||||||
|
}
|
||||||
|
async function twsDoDelete(){
|
||||||
|
const wid=S.twsConfirmDel;if(!wid)return;
|
||||||
|
S.twsConfirmDel=null;
|
||||||
|
try{await $api('/terraform/workspaces/'+wid,{method:'DELETE'})}catch(e){alert('Erro: '+e.message)}
|
||||||
|
tfWsLoadList();
|
||||||
|
}
|
||||||
|
async function twsCancel(wid){
|
||||||
|
try{await $api('/terraform/workspaces/'+wid+'/cancel',{method:'POST'})}catch(e){}
|
||||||
|
tfWsLoadList();
|
||||||
|
}
|
||||||
|
async function _twsPollWs(wid){
|
||||||
|
for(let i=0;i<300;i++){
|
||||||
|
await new Promise(r=>setTimeout(r,3000));
|
||||||
|
try{
|
||||||
|
const r=await $api('/terraform/workspaces/'+wid+'/status');
|
||||||
|
const done=!['planning','applying','destroying'].includes(r.status);
|
||||||
|
const idx=S.twsList.findIndex(w=>w.id===wid);
|
||||||
|
if(idx>=0){
|
||||||
|
S.twsList[idx].status=r.status;
|
||||||
|
S.twsList[idx].plan_output=r.plan_output||S.twsList[idx].plan_output;
|
||||||
|
S.twsList[idx].apply_output=r.apply_output||S.twsList[idx].apply_output;
|
||||||
|
S.twsList[idx].destroy_output=r.destroy_output||S.twsList[idx].destroy_output;
|
||||||
|
S.twsList[idx].error=r.error||'';
|
||||||
|
if(S.tab==='tf-workspaces'){
|
||||||
|
const el=document.getElementById('tws-badge-'+wid);
|
||||||
|
if(el)el.outerHTML=_tfBadge(r.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(done){tfWsLoadList();return}
|
||||||
|
}catch(e){}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ── OCI Explorer ── */
|
/* ── OCI Explorer ── */
|
||||||
const EXP_GROUPS=[
|
const EXP_GROUPS=[
|
||||||
{name:'Compute',tabs:[
|
{name:'Compute',tabs:[
|
||||||
@@ -1858,17 +2142,17 @@ function rExplorer(){
|
|||||||
const catIcons={Compute:IC.computer,Rede:IC.globe,Storage:IC.hdd,Database:IC.db,Containers:IC.container,Serverless:IC.zap,Observabilidade:IC.activity,Segurança:IC.shield,IAM:IC.user};
|
const catIcons={Compute:IC.computer,Rede:IC.globe,Storage:IC.hdd,Database:IC.db,Containers:IC.container,Serverless:IC.zap,Observabilidade:IC.activity,Segurança:IC.shield,IAM:IC.user};
|
||||||
const cats=EXP_GROUPS.map(g=>`<div class="exp-cat${S.expCat===g.name?' active':''}" onclick="expSetCat('${g.name}')">${catIcons[g.name]||''} ${g.name}</div>`).join('');
|
const cats=EXP_GROUPS.map(g=>`<div class="exp-cat${S.expCat===g.name?' active':''}" onclick="expSetCat('${g.name}')">${catIcons[g.name]||''} ${g.name}</div>`).join('');
|
||||||
const activeGrp=EXP_GROUPS.find(g=>g.name===S.expCat)||EXP_GROUPS[0];
|
const activeGrp=EXP_GROUPS.find(g=>g.name===S.expCat)||EXP_GROUPS[0];
|
||||||
const tabs=activeGrp.tabs.map(t=>{const cnt=S.expCounts[t.key];return`<div class="exp-tab${S.expResType===t.key?' active':''}" onclick="expSetTab('${t.key}')">${t.label}${cnt!=null?' <span style="font-size:.58rem;opacity:.7">('+cnt+')</span>':''}</div>`}).join('');
|
const tabs=activeGrp.tabs.map(t=>{const cnt=S.expCounts[t.key];return`<div class="exp-tab${S.expResType===t.key?' active':''}" onclick="expSetTab('${t.key}')">${t.label} <span id="exp-tab-cnt-${t.key}" style="font-size:.58rem;opacity:.7">${cnt!=null?'('+cnt+')':''}</span></div>`}).join('');
|
||||||
// KPI stats from counts
|
// KPI stats from counts
|
||||||
const _iamKeys=new Set(['iam_users','iam_groups','iam_policies','iam_dynamic_groups']);
|
const _iamKeys=new Set(['iam_users','iam_groups','iam_policies','iam_dynamic_groups']);
|
||||||
const totalRes=Object.entries(S.expCounts).reduce((a,[k,v])=>a+(_iamKeys.has(k)?0:(v||0)),0);
|
const totalRes=Object.entries(S.expCounts).reduce((a,[k,v])=>a+(_iamKeys.has(k)?0:(v||0)),0);
|
||||||
const kpiCats=EXP_GROUPS.map(g=>{const cnt=g.tabs.reduce((a,t)=>a+(S.expCounts[t.key]||0),0);const detail=g.tabs.filter(t=>(S.expCounts[t.key]||0)>0).map(t=>t.label+': '+S.expCounts[t.key]).join('\n');return{name:g.name,icon:catIcons[g.name]||'',count:cnt,detail:detail}}).filter(k=>k.count>0);
|
const kpiCats=EXP_GROUPS.map(g=>{const cnt=g.tabs.reduce((a,t)=>a+(S.expCounts[t.key]||0),0);const detail=g.tabs.filter(t=>(S.expCounts[t.key]||0)>0).map(t=>t.label+': '+S.expCounts[t.key]).join('\n');return{name:g.name,icon:catIcons[g.name]||'',count:cnt,detail:detail}}).filter(k=>k.count>0);
|
||||||
const selCompName=S.expTree.find(n=>n.id===S.expSelComp);
|
const selCompName=S.expTree.find(n=>n.id===S.expSelComp);
|
||||||
const compLabel=selCompName?selCompName.name:'—';
|
const compLabel=selCompName?selCompName.name:'—';
|
||||||
const kpiHtml=S.expSelComp?`<div style="display:flex;gap:.5rem;padding:.5rem .75rem;border-bottom:1px solid var(--bd);background:var(--bg);overflow-x:auto;align-items:stretch">
|
const kpiHtml=S.expSelComp?`<div id="exp-kpi" style="display:flex;gap:.5rem;padding:.5rem .75rem;border-bottom:1px solid var(--bd);background:var(--bg);overflow-x:auto;align-items:stretch">
|
||||||
<div class="kpi" style="padding:.5rem .7rem;min-width:120px;flex:1"><div class="kpi-label">${IC.folder} Compartment</div><div style="font-size:.82rem;font-weight:700;color:var(--t1);margin-top:.15rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${compLabel}">${compLabel}</div><div class="kpi-sub">${S.expSelRegions.length||'Todas'} região(ões)</div></div>
|
<div class="kpi" style="padding:.5rem .7rem;min-width:120px;flex:1"><div class="kpi-label">${IC.folder} Compartment</div><div style="font-size:.82rem;font-weight:700;color:var(--t1);margin-top:.15rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${compLabel}">${compLabel}</div><div class="kpi-sub">${S.expSelRegions.length||'Todas'} região(ões)</div></div>
|
||||||
<div class="kpi kpi-info" style="padding:.5rem .7rem;min-width:80px;text-align:center;flex:0 0 auto"><div class="kpi-label">${IC.activity} Total</div><div class="kpi-val v-info" style="font-size:1.3rem">${totalRes}</div><div class="kpi-sub">${S.expCountsRefreshing?'<span class="spinner" style="width:10px;height:10px;display:inline-block;vertical-align:-1px"></span> atualizando...':'recursos'}</div></div>
|
<div class="kpi kpi-info" style="padding:.5rem .7rem;min-width:80px;text-align:center;flex:0 0 auto"><div class="kpi-label">${IC.activity} Total</div><div id="exp-kpi-total" class="kpi-val v-info" style="font-size:1.3rem">${totalRes}</div><div id="exp-kpi-sub" class="kpi-sub">${S.expCountsRefreshing?'<span class="spinner" style="width:10px;height:10px;display:inline-block;vertical-align:-1px"></span> atualizando...':'recursos'}</div></div>
|
||||||
${kpiCats.slice(0,6).map(k=>`<div class="kpi" style="padding:.5rem .7rem;min-width:75px;text-align:center;flex:0 0 auto;cursor:default" title="${k.detail}"><div class="kpi-label" style="justify-content:center">${k.icon}</div><div class="kpi-val" style="font-size:1.2rem">${k.count}</div><div class="kpi-sub">${k.name}</div></div>`).join('')}
|
<span id="exp-kpi-cats">${kpiCats.slice(0,6).map(k=>`<div class="kpi" style="padding:.5rem .7rem;min-width:75px;text-align:center;flex:0 0 auto;cursor:default" title="${k.detail}"><div class="kpi-label" style="justify-content:center">${k.icon}</div><div class="kpi-val" style="font-size:1.2rem">${k.count}</div><div class="kpi-sub">${k.name}</div></div>`).join('')}</span>
|
||||||
</div>`:'';
|
</div>`:'';
|
||||||
return`<div class="exp-wrap">
|
return`<div class="exp-wrap">
|
||||||
<div class="exp-tree" id="exp-tree"><div style="padding:.6rem .7rem .5rem;font-size:.78rem;font-weight:700;color:var(--t1);border-bottom:1px solid var(--bd);margin-bottom:.3rem;display:flex;align-items:center;gap:.4rem;background:var(--bg2)">${IC.folder} Compartments</div>${S.expTree.length?rExpTree(null,0):'<div class="ld"><span class="spinner"></span> Carregando...</div>'}</div>
|
<div class="exp-tree" id="exp-tree"><div style="padding:.6rem .7rem .5rem;font-size:.78rem;font-weight:700;color:var(--t1);border-bottom:1px solid var(--bd);margin-bottom:.3rem;display:flex;align-items:center;gap:.4rem;background:var(--bg2)">${IC.folder} Compartments</div>${S.expTree.length?rExpTree(null,0):'<div class="ld"><span class="spinner"></span> Carregando...</div>'}</div>
|
||||||
@@ -1945,31 +2229,44 @@ async function expLoadRes(){
|
|||||||
S.expCounts[S.expResType]=all.length;
|
S.expCounts[S.expResType]=all.length;
|
||||||
S.expLoading=false;R()}
|
S.expLoading=false;R()}
|
||||||
let _expCountAbort=0;
|
let _expCountAbort=0;
|
||||||
async function expLoadCachedCounts(){
|
async function expLoadCachedCounts(soft){
|
||||||
const comp=S.expSelComp;const cfg=S.expCfg;if(!comp||!cfg)return;
|
const comp=S.expSelComp;const cfg=S.expCfg;if(!comp||!cfg)return;
|
||||||
const regKey=S.expSelRegions.length?S.expSelRegions.slice().sort().join(','):'';
|
const regKey=S.expSelRegions.length?S.expSelRegions.slice().sort().join(','):'';
|
||||||
try{
|
try{
|
||||||
const q=new URLSearchParams({compartment_id:comp,regions:regKey});
|
const q=new URLSearchParams({compartment_id:comp,regions:regKey});
|
||||||
const d=await $api('/oci/explore/'+cfg+'/counts?'+q);
|
const d=await $api('/oci/explore/'+cfg+'/counts?'+q);
|
||||||
if(d&&!d.error){for(const[k,v]of Object.entries(d)){S.expCounts[k]=v.count}R()}
|
if(d&&!d.error){let changed=false;for(const[k,v]of Object.entries(d)){if(S.expCounts[k]!==v.count){S.expCounts[k]=v.count;changed=true}}if(changed){if(soft)expUpdateKpi();else R()}}
|
||||||
}catch(e){console.warn('Cache counts error:',e)}
|
}catch(e){console.warn('Cache counts error:',e)}
|
||||||
}
|
}
|
||||||
|
function expUpdateKpi(){
|
||||||
|
const el=document.getElementById('exp-kpi');if(!el)return;
|
||||||
|
const catIcons={Compute:IC.computer,Rede:IC.globe,Storage:IC.hdd,Database:IC.db,Containers:IC.container,Serverless:IC.zap,Observabilidade:IC.activity,'Segurança':IC.shield,IAM:IC.user};
|
||||||
|
const _iamKeys=new Set(['iam_users','iam_groups','iam_policies','iam_dynamic_groups']);
|
||||||
|
const totalRes=Object.entries(S.expCounts).reduce((a,[k,v])=>a+(_iamKeys.has(k)?0:(v||0)),0);
|
||||||
|
const kpiCats=EXP_GROUPS.map(g=>{const cnt=g.tabs.reduce((a,t)=>a+(S.expCounts[t.key]||0),0);const detail=g.tabs.filter(t=>(S.expCounts[t.key]||0)>0).map(t=>t.label+': '+S.expCounts[t.key]).join('\n');return{name:g.name,icon:catIcons[g.name]||'',count:cnt,detail}}).filter(k=>k.count>0);
|
||||||
|
const totalEl=document.getElementById('exp-kpi-total');if(totalEl)totalEl.textContent=totalRes;
|
||||||
|
const subEl=document.getElementById('exp-kpi-sub');if(subEl)subEl.innerHTML=S.expCountsRefreshing?'<span class="spinner" style="width:10px;height:10px;display:inline-block;vertical-align:-1px"></span> atualizando...':'recursos';
|
||||||
|
const catsEl=document.getElementById('exp-kpi-cats');if(catsEl)catsEl.innerHTML=kpiCats.slice(0,6).map(k=>`<div class="kpi" style="padding:.5rem .7rem;min-width:75px;text-align:center;flex:0 0 auto;cursor:default" title="${k.detail}"><div class="kpi-label" style="justify-content:center">${k.icon}</div><div class="kpi-val" style="font-size:1.2rem">${k.count}</div><div class="kpi-sub">${k.name}</div></div>`).join('');
|
||||||
|
// Update tab counts
|
||||||
|
const activeGrp=EXP_GROUPS.find(g=>g.name===S.expCat)||EXP_GROUPS[0];
|
||||||
|
activeGrp.tabs.forEach(t=>{const te=document.getElementById('exp-tab-cnt-'+t.key);if(te)te.textContent=S.expCounts[t.key]!=null?'('+S.expCounts[t.key]+')':''});
|
||||||
|
}
|
||||||
async function expRefreshCounts(){
|
async function expRefreshCounts(){
|
||||||
const comp=S.expSelComp;const cfg=S.expCfg;if(!comp||!cfg)return;
|
const comp=S.expSelComp;const cfg=S.expCfg;if(!comp||!cfg)return;
|
||||||
const myRun=++_expCountAbort;
|
const myRun=++_expCountAbort;
|
||||||
S.expCountsRefreshing=true;R();
|
S.expCountsRefreshing=true;expUpdateKpi();
|
||||||
try{
|
try{
|
||||||
await $api('/oci/explore/'+cfg+'/counts/refresh',{method:'POST',body:{compartment_id:comp,regions:S.expSelRegions}});
|
await $api('/oci/explore/'+cfg+'/counts/refresh',{method:'POST',body:{compartment_id:comp,regions:S.expSelRegions}});
|
||||||
const poll=async(attempts)=>{
|
const poll=async(attempts)=>{
|
||||||
if(_expCountAbort!==myRun||S.expSelComp!==comp){S.expCountsRefreshing=false;R();return}
|
if(_expCountAbort!==myRun||S.expSelComp!==comp){S.expCountsRefreshing=false;expUpdateKpi();return}
|
||||||
if(attempts<=0){S.expCountsRefreshing=false;R();return}
|
if(attempts<=0){S.expCountsRefreshing=false;expUpdateKpi();return}
|
||||||
await new Promise(r=>setTimeout(r,3000));
|
await new Promise(r=>setTimeout(r,3000));
|
||||||
if(_expCountAbort!==myRun||S.expSelComp!==comp){S.expCountsRefreshing=false;R();return}
|
if(_expCountAbort!==myRun||S.expSelComp!==comp){S.expCountsRefreshing=false;expUpdateKpi();return}
|
||||||
await expLoadCachedCounts();
|
await expLoadCachedCounts(true);
|
||||||
poll(attempts-1);
|
poll(attempts-1);
|
||||||
};
|
};
|
||||||
poll(12);
|
poll(12);
|
||||||
}catch(e){S.expCountsRefreshing=false;R();console.warn('Refresh counts error:',e)}
|
}catch(e){S.expCountsRefreshing=false;expUpdateKpi();console.warn('Refresh counts error:',e)}
|
||||||
}
|
}
|
||||||
function renderExpData(d){if(!d)return'<div class="emp"><p>Selecione um compartment na árvore.</p></div>';
|
function renderExpData(d){if(!d)return'<div class="emp"><p>Selecione um compartment na árvore.</p></div>';
|
||||||
if(d.error)return`<div class="al al-e">${d.error}</div>`;if(!Array.isArray(d)||!d.length)return'<div class="emp"><p>Nenhum recurso encontrado neste compartment.</p></div>';
|
if(d.error)return`<div class="al al-e">${d.error}</div>`;if(!Array.isArray(d)||!d.length)return'<div class="emp"><p>Nenhum recurso encontrado neste compartment.</p></div>';
|
||||||
@@ -2547,9 +2844,9 @@ function dlRpt(id,f){window.open(API+'/reports/'+id+'/download?fmt='+f+'&token='
|
|||||||
async function refreshDl(){S.reports=await $api('/reports');S.reportFiles={};S.dlExpandedRid=null;R()}
|
async function refreshDl(){S.reports=await $api('/reports');S.reportFiles={};S.dlExpandedRid=null;R()}
|
||||||
|
|
||||||
/* ── Edit helpers ── */
|
/* ── Edit helpers ── */
|
||||||
function editCfg(type,id){S.editing={type,id};if(type==='oci'){const c=S.ociCfg.find(x=>x.id===id);S.ociFormRegVal=c?c.region:'';S.ociFormRegFilter=''}R();
|
function editCfg(type,id){S.editing={type,id};if(type==='oci'){const c=S.ociCfg.find(x=>x.id===id);S.ociFormRegVal=c?c.region:'';S.ociFormRegFilter='';S.ociFormOpen=true;S.ociAuthType=c?.auth_type||'api_key'}R();
|
||||||
const form=document.querySelector('.cd:has(#obtn), .cd:has(#gbtn), .cd:has(#abtn), .cd:has(#mcbtn)');if(form)form.scrollIntoView({behavior:'smooth',block:'start'})}
|
const form=document.querySelector('.cd:has(#obtn), .cd:has(#gbtn), .cd:has(#abtn), .cd:has(#mcbtn)');if(form)form.scrollIntoView({behavior:'smooth',block:'start'})}
|
||||||
function cancelEdit(){S.editing=null;S.ociFormRegVal='';S.ociFormRegFilter='';R()}
|
function cancelEdit(){S.editing=null;S.ociFormRegVal='';S.ociFormRegFilter='';S.ociFormOpen=false;S.ociAuthType='api_key';R()}
|
||||||
|
|
||||||
/* ── OCI Config ── */
|
/* ── OCI Config ── */
|
||||||
function rOciRegionDd(eo){
|
function rOciRegionDd(eo){
|
||||||
@@ -2569,6 +2866,10 @@ ${ddItems||'<div style="padding:.5rem;color:var(--t4);font-size:.72rem">Nenhuma
|
|||||||
</div>`:''}</div>`}
|
</div>`:''}</div>`}
|
||||||
function ociFormPickReg(r){S.ociFormRegVal=r;S.ociFormRegOpen=false;S.ociFormRegFilter='';R()}
|
function ociFormPickReg(r){S.ociFormRegVal=r;S.ociFormRegOpen=false;S.ociFormRegFilter='';R()}
|
||||||
function rOci(){const eo=S.editing?.type==='oci'?S.ociCfg.find(c=>c.id===S.editing.id):null;
|
function rOci(){const eo=S.editing?.type==='oci'?S.ociCfg.find(c=>c.id===S.editing.id):null;
|
||||||
|
const formOpen=S.ociFormOpen||!!eo;
|
||||||
|
const at=eo?(eo.auth_type||'api_key'):S.ociAuthType;
|
||||||
|
const isToken=at==='session_token';
|
||||||
|
const authTag=t=>(t||'api_key')==='session_token'?'<span class="tag" style="background:#e67e2222;color:#e67e22;font-size:.58rem">Session Token</span>':'<span class="tag" style="background:var(--gnl);color:var(--gn);font-size:.58rem">API Key</span>';
|
||||||
return`<div class="cd" style="display:flex;align-items:center;justify-content:space-between;padding:.6rem 1rem">
|
return`<div class="cd" style="display:flex;align-items:center;justify-content:space-between;padding:.6rem 1rem">
|
||||||
<div style="font-size:.78rem;font-weight:600;color:var(--t2)">${IC.settings} Configuração do Sistema</div>
|
<div style="font-size:.78rem;font-weight:600;color:var(--t2)">${IC.settings} Configuração do Sistema</div>
|
||||||
<div style="display:flex;gap:.4rem;align-items:center">
|
<div style="display:flex;gap:.4rem;align-items:center">
|
||||||
@@ -2578,44 +2879,71 @@ function rOci(){const eo=S.editing?.type==='oci'?S.ociCfg.find(c=>c.id===S.editi
|
|||||||
</div></div>
|
</div></div>
|
||||||
<div id="cfgMsg"></div>
|
<div id="cfgMsg"></div>
|
||||||
<div class="cd"><div class="ct">${IC.cloud} Credenciais Registradas</div><div id="ocm"></div>
|
<div class="cd"><div class="ct">${IC.cloud} Credenciais Registradas</div><div id="ocm"></div>
|
||||||
<table><thead><tr><th>Tenancy</th><th>Region</th><th>Detalhes</th><th>Ações</th></tr></thead><tbody>
|
<table><thead><tr><th>Tenancy</th><th>Tipo</th><th>Region</th><th>Detalhes</th><th>Ações</th></tr></thead><tbody>
|
||||||
${S.ociCfg.map(c=>`<tr${eo&&eo.id===c.id?' style="background:var(--bg3)"':''}><td><strong>${c.tenancy_name}</strong><br><span style="font-size:.66rem;color:var(--t4)">${c.created_at}</span></td><td><span class="tag">${c.region}</span></td>
|
${S.ociCfg.map(c=>`<tr${eo&&eo.id===c.id?' style="background:var(--bg3)"':''}><td><strong>${c.tenancy_name}</strong><br><span style="font-size:.66rem;color:var(--t4)">${c.created_at}</span></td><td>${authTag(c.auth_type)}</td><td><span class="tag">${c.region}</span></td>
|
||||||
<td style="font-family:var(--fm);font-size:.66rem;color:var(--t4);line-height:1.6"><span title="Fingerprint">${IC.key} ••••••••••••••••••••</span><br><span title="Tenancy OCID">${IC.building} ${c.tenancy_ocid}</span><br><span title="User OCID">${IC.user} ${c.user_ocid}</span><br><span title="Compartment">${IC.pkg} ${c.compartment_id||'—'}</span><br><span title="SSH Public Key">${IC.key} SSH: ${c.ssh_public_key_preview||'<em>não configurada</em>'}</span></td>
|
<td style="font-family:var(--fm);font-size:.66rem;color:var(--t4);line-height:1.6">${(c.auth_type||'api_key')==='session_token'?`<span title="Tenancy OCID">${IC.building} ${c.tenancy_ocid}</span><br><span title="Auth">${IC.lock} Session Token</span>`:`<span title="Fingerprint">${IC.key} ••••••••••••••••••••</span><br><span title="Tenancy OCID">${IC.building} ${c.tenancy_ocid}</span><br><span title="User OCID">${IC.user} ${c.user_ocid}</span>`}<br><span title="Compartment">${IC.pkg} ${c.compartment_id||'—'}</span><br><span title="SSH Public Key">${IC.key} SSH: ${c.ssh_public_key_preview||'<em>não configurada</em>'}</span></td>
|
||||||
<td><button class="btn bs bsm" onclick="editCfg('oci','${c.id}')">Editar</button> <button class="btn bs bsm" onclick="tOci('${c.id}')">Testar</button> <button class="btn bd bsm" onclick="dOci('${c.id}')">Excluir</button></td></tr>`).join('')}</tbody></table>
|
<td><button class="btn bs bsm" onclick="editCfg('oci','${c.id}')">Editar</button> <button class="btn bs bsm" onclick="tOci('${c.id}')">Testar</button> <button class="btn bd bsm" onclick="dOci('${c.id}')">Excluir</button></td></tr>`).join('')}</tbody></table>
|
||||||
${!S.ociCfg.length?'<div class="emp" style="padding:.85rem"><p>Nenhuma credencial registrada.</p></div>':''}</div>
|
${!S.ociCfg.length?'<div class="emp" style="padding:.85rem"><p>Nenhuma credencial registrada.</p></div>':''}</div>
|
||||||
<div class="cd"><div class="ct">${eo?IC.edit+' Editar Credencial':IC.plus+' Nova Credencial'}</div>
|
<div class="cd">
|
||||||
<div class="cdesc">${eo?'Editando: <strong>'+eo.tenancy_name+'</strong>':'Adicione as credenciais da sua conta OCI para executar reports e acessar serviços.'}</div><div id="om"></div>
|
<div style="display:flex;align-items:center;cursor:pointer;gap:.5rem" onclick="${eo?'':`S.ociFormOpen=!S.ociFormOpen;R()`}">
|
||||||
|
<svg viewBox="0 0 16 16" width="14" height="14" fill="var(--t3)" style="transition:transform .2s;transform:rotate(${formOpen?'90':'0'}deg)"><path d="M6 3l5 5-5 5z"/></svg>
|
||||||
|
<div class="ct" style="margin:0">${eo?IC.edit+' Editar Credencial':IC.plus+' Nova Credencial'}</div>
|
||||||
|
</div>
|
||||||
|
${formOpen?`
|
||||||
|
<div class="cdesc" style="margin-top:.5rem">${eo?'Editando: <strong>'+eo.tenancy_name+'</strong>':'Adicione as credenciais da sua conta OCI para executar reports e acessar serviços.'}</div><div id="om"></div>
|
||||||
|
<div style="display:flex;gap:.5rem;margin-bottom:.7rem">
|
||||||
|
<button class="tws-filter-btn${!isToken?' on':''}" onclick="S.ociAuthType='api_key';R()" ${eo?'disabled':''}>${IC.key} API Key</button>
|
||||||
|
<button class="tws-filter-btn${isToken?' on':''}" onclick="S.ociAuthType='session_token';R()" ${eo?'disabled':''}>${IC.lock} Session Token</button>
|
||||||
|
${eo?'<span style="font-size:.62rem;color:var(--t4);align-self:center">(tipo não pode ser alterado)</span>':''}
|
||||||
|
</div>
|
||||||
<div class="g2">
|
<div class="g2">
|
||||||
<div class="ig"><label>Tenancy Name</label><input type="text" id="otn" placeholder="minha-empresa" value="${eo?eo.tenancy_name:''}"></div>
|
<div class="ig"><label>Tenancy Name</label><input type="text" id="otn" placeholder="minha-empresa" value="${eo?eo.tenancy_name:''}"></div>
|
||||||
<div class="ig"><label>OCID User</label><input type="password" id="ouo" placeholder="ocid1.user.oc1.." value="${eo?'':''}" autocomplete="off">${eo?'<div class="ht">Preencha para alterar. Atual: <code>'+eo.user_ocid+'</code></div>':''}</div>
|
|
||||||
<div class="ig"><label>Fingerprint</label><input type="password" id="ofp" placeholder="aa:bb:cc:dd:..." value="${eo?'':''}" autocomplete="off">${eo?'<div class="ht">Preencha para alterar. Atual: <code>'+eo.fingerprint+'</code></div>':''}</div>
|
|
||||||
<div class="ig"><label>OCID Tenancy</label><input type="password" id="oto" placeholder="ocid1.tenancy.oc1.." value="${eo?'':''}" autocomplete="off">${eo?'<div class="ht">Preencha para alterar. Atual: <code>'+eo.tenancy_ocid+'</code></div>':''}</div>
|
<div class="ig"><label>OCID Tenancy</label><input type="password" id="oto" placeholder="ocid1.tenancy.oc1.." value="${eo?'':''}" autocomplete="off">${eo?'<div class="ht">Preencha para alterar. Atual: <code>'+eo.tenancy_ocid+'</code></div>':''}</div>
|
||||||
<div class="ig"><label>Region</label>${rOciRegionDd(eo)}</div>
|
<div class="ig"><label>Region</label>${rOciRegionDd(eo)}</div>
|
||||||
<div class="ig"><label>Compartment OCID</label><input type="password" id="ocp" placeholder="ocid1.compartment.oc1.." value="${eo?'':''}" autocomplete="off">${eo?'<div class="ht">Preencha para alterar. Atual: <code>'+(eo.compartment_id||'—')+'</code></div>':''}</div></div>
|
<div class="ig"><label>Compartment OCID</label><input type="password" id="ocp" placeholder="ocid1.compartment.oc1.." value="${eo?'':''}" autocomplete="off">${eo?'<div class="ht">Preencha para alterar. Atual: <code>'+(eo.compartment_id||'—')+'</code></div>':''}</div></div>
|
||||||
<div class="g2"><div class="ig"><label>Private Key (.pem)</label>${eo?'<div class="ht">Deixe vazio para manter a chave atual</div>':''}<input type="file" id="osk" accept=".pem"></div>
|
${!isToken?`<div class="g2">
|
||||||
<div class="ig"><label>Public Key (.pem)</label><input type="file" id="opk" accept=".pem"></div></div>
|
<div class="ig"><label>OCID User</label><input type="password" id="ouo" placeholder="ocid1.user.oc1.." value="" autocomplete="off">${eo?'<div class="ht">Preencha para alterar. Atual: <code>'+eo.user_ocid+'</code></div>':''}</div>
|
||||||
<div class="g2"><div class="ig"><label>Key Passphrase</label><div class="ht">Apenas se a chave privada for protegida por senha</div><input type="password" id="okp" placeholder="(opcional)"></div>
|
<div class="ig"><label>Fingerprint</label><input type="password" id="ofp" placeholder="aa:bb:cc:dd:..." value="" autocomplete="off">${eo?'<div class="ht">Preencha para alterar. Atual: <code>'+eo.fingerprint+'</code></div>':''}</div></div>`
|
||||||
|
:`<div class="g2">
|
||||||
|
<div class="ig"><label>Session Token</label><div class="ht">Cole o conteúdo do arquivo token gerado por <code>oci session authenticate</code></div><textarea id="ostk" rows="3" placeholder="Conteúdo do arquivo ~/.oci/sessions/DEFAULT/token" style="font-size:.68rem;font-family:var(--fm);resize:vertical"></textarea></div>
|
||||||
|
<div class="ig"><label>OCID User <span style="font-size:.6rem;color:var(--t4)">(opcional)</span></label><input type="password" id="ouo" placeholder="ocid1.user.oc1.. (preenchido automaticamente)" value="" autocomplete="off"><div class="ht">O SDK pode resolver o user a partir do token. Deixe vazio se preferir.</div></div></div>`}
|
||||||
|
<div class="g2"><div class="ig"><label>${isToken?'Chave de Sessão (.pem)':'Private Key (.pem)'}</label>${eo?'<div class="ht">Deixe vazio para manter a chave atual</div>':''}<div class="ht">${isToken?'Arquivo <code>oci_api_key.pem</code> da pasta de sessão':'Chave privada da API Key OCI'}</div><input type="file" id="osk" accept=".pem"></div>
|
||||||
|
${!isToken?'<div class="ig"><label>Public Key (.pem)</label><input type="file" id="opk" accept=".pem"></div>':'<div class="ig"></div>'}</div>
|
||||||
|
<div class="g2">${!isToken?'<div class="ig"><label>Key Passphrase</label><div class="ht">Apenas se a chave privada for protegida por senha</div><input type="password" id="okp" placeholder="(opcional)"></div>':'<div class="ig"></div>'}
|
||||||
<div class="ig"><label>SSH Public Key</label><div class="ht">Chave SSH para acesso a instâncias compute (ex: ssh-rsa AAAA...)</div><textarea id="osshk" rows="2" placeholder="ssh-rsa AAAAB3NzaC1yc2E... (opcional)" style="font-size:.72rem;font-family:var(--fm);resize:vertical">${eo&&eo.ssh_public_key?eo.ssh_public_key:''}</textarea>${eo&&eo.ssh_public_key_preview?'<div class="ht">Atual: <code>'+eo.ssh_public_key_preview+'</code></div>':''}</div></div>
|
<div class="ig"><label>SSH Public Key</label><div class="ht">Chave SSH para acesso a instâncias compute (ex: ssh-rsa AAAA...)</div><textarea id="osshk" rows="2" placeholder="ssh-rsa AAAAB3NzaC1yc2E... (opcional)" style="font-size:.72rem;font-family:var(--fm);resize:vertical">${eo&&eo.ssh_public_key?eo.ssh_public_key:''}</textarea>${eo&&eo.ssh_public_key_preview?'<div class="ht">Atual: <code>'+eo.ssh_public_key_preview+'</code></div>':''}</div></div>
|
||||||
<div style="display:flex;gap:8px"><button class="btn bp" id="obtn" onclick="sOci()">${eo?'Salvar Alterações':'Salvar Credencial'}</button>${eo?'<button class="btn bs" onclick="cancelEdit()">Cancelar</button>':''}</div></div>
|
<div style="display:flex;gap:8px"><button class="btn bp" id="obtn" onclick="sOci()">${eo?'Salvar Alterações':'Salvar Credencial'}</button>${eo?'<button class="btn bs" onclick="cancelEdit()">Cancelar</button>':''}</div>
|
||||||
|
`:''}
|
||||||
|
</div>
|
||||||
`+rConfigLogs('oci')}
|
`+rConfigLogs('oci')}
|
||||||
async function sOci(){const tn=document.getElementById('otn').value;
|
async function sOci(){const tn=document.getElementById('otn').value;
|
||||||
if(!tn)return sm('om','Preencha o Tenancy Name','e');
|
if(!tn)return sm('om','Preencha o Tenancy Name','e');
|
||||||
const fd=new FormData();fd.append('tenancy_name',tn);fd.append('tenancy_ocid',document.getElementById('oto').value);
|
|
||||||
fd.append('user_ocid',document.getElementById('ouo').value);fd.append('fingerprint',document.getElementById('ofp').value);
|
|
||||||
fd.append('region',S.ociFormRegVal||'');fd.append('compartment_id',document.getElementById('ocp').value);
|
|
||||||
fd.append('key_passphrase',document.getElementById('okp').value||'');
|
|
||||||
fd.append('ssh_public_key',document.getElementById('osshk').value||'');
|
|
||||||
const sk=document.getElementById('osk').files[0];
|
|
||||||
const isEdit=S.editing?.type==='oci';
|
const isEdit=S.editing?.type==='oci';
|
||||||
|
const eo=isEdit?S.ociCfg.find(c=>c.id===S.editing.id):null;
|
||||||
|
const at=eo?(eo.auth_type||'api_key'):S.ociAuthType;
|
||||||
|
const isToken=at==='session_token';
|
||||||
|
const fd=new FormData();fd.append('tenancy_name',tn);fd.append('tenancy_ocid',document.getElementById('oto').value);
|
||||||
|
fd.append('user_ocid',(document.getElementById('ouo')||{}).value||'');
|
||||||
|
fd.append('fingerprint',(document.getElementById('ofp')||{}).value||'');
|
||||||
|
fd.append('region',S.ociFormRegVal||'');fd.append('compartment_id',document.getElementById('ocp').value);
|
||||||
|
fd.append('key_passphrase',(document.getElementById('okp')||{}).value||'');
|
||||||
|
fd.append('ssh_public_key',document.getElementById('osshk').value||'');
|
||||||
|
fd.append('auth_type',at);
|
||||||
|
fd.append('security_token',(document.getElementById('ostk')||{}).value||'');
|
||||||
|
const sk=document.getElementById('osk').files[0];
|
||||||
if(!isEdit){
|
if(!isEdit){
|
||||||
if(!document.getElementById('oto').value||!document.getElementById('ouo').value||!document.getElementById('ofp').value)return sm('om','Preencha OCID Tenancy, OCID User e Fingerprint','e');
|
if(!document.getElementById('oto').value)return sm('om','Preencha o OCID Tenancy','e');
|
||||||
if(!sk)return sm('om','Selecione a chave privada','e');
|
if(!isToken){
|
||||||
|
if(!document.getElementById('ouo').value||!document.getElementById('ofp').value)return sm('om','Preencha OCID User e Fingerprint','e');
|
||||||
|
}else{
|
||||||
|
if(!(document.getElementById('ostk')||{}).value)return sm('om','Cole o Session Token','e');
|
||||||
|
}
|
||||||
|
if(!sk)return sm('om',isToken?'Selecione a chave de sessão (.pem)':'Selecione a chave privada (.pem)','e');
|
||||||
}
|
}
|
||||||
if(sk)fd.append('private_key',sk);
|
if(sk)fd.append('private_key',sk);
|
||||||
const pk=document.getElementById('opk').files[0];if(pk)fd.append('public_key',pk);
|
const pk=(document.getElementById('opk')||{}).files?.[0];if(pk)fd.append('public_key',pk);
|
||||||
const btn=document.getElementById('obtn');btn.disabled=true;btn.textContent='Salvando...';sm('om','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Salvando credencial...','i');
|
const btn=document.getElementById('obtn');btn.disabled=true;btn.textContent='Salvando...';sm('om','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Salvando credencial...','i');
|
||||||
try{const url=isEdit?'/oci/configs/'+S.editing.id:'/oci/config';const method=isEdit?'PUT':'POST';
|
try{const url=isEdit?'/oci/configs/'+S.editing.id:'/oci/config';const method=isEdit?'PUT':'POST';
|
||||||
await $api(url,{method,body:fd,headers:{}});S.editing=null;S.ociFormRegVal='';S.ociFormRegFilter='';S.ociCfg=await $api('/oci/configs');sm('om',IC.ok+' Credencial '+(isEdit?'atualizada':'salva')+' com sucesso!','s');R()}catch(e){sm('om',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Salvar Credencial'}}
|
await $api(url,{method,body:fd,headers:{}});S.editing=null;S.ociFormRegVal='';S.ociFormRegFilter='';S.ociFormOpen=false;S.ociAuthType='api_key';S.ociCfg=await $api('/oci/configs');sm('om',IC.ok+' Credencial '+(isEdit?'atualizada':'salva')+' com sucesso!','s');R()}catch(e){sm('om',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Salvar Credencial'}}
|
||||||
async function cfgExport(){
|
async function cfgExport(){
|
||||||
try{
|
try{
|
||||||
const resp=await fetch(API+'/config/export',{headers:S.token?{'Authorization':'Bearer '+S.token}:{}});
|
const resp=await fetch(API+'/config/export',{headers:S.token?{'Authorization':'Bearer '+S.token}:{}});
|
||||||
|
|||||||
Reference in New Issue
Block a user