From 18fabec805306cef2c0d63d0f2b34333bb37232d Mon Sep 17 00:00:00 2001
From: nogueiraguh
Date: Thu, 12 Mar 2026 11:24:38 -0300
Subject: [PATCH] feat: Explorer KPI stats, enhanced views, start/stop
expansion, Prompt Generator docs injection
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Explorer: KPI stats bar with cached counts (SQLite), background refresh,
enhanced views for VCNs/Subnets/LBs/Buckets, start/stop for DB Systems,
MySQL, Container Instances, dead state filtering across all 36 endpoints
- Prompt Generator: same knowledge pipeline as Terraform Agent — resource
type detection + official docs from GitHub + compact resource reference
- Fix: NetworkFirewallPolicySummaryCollection not iterable (.items accessor)
---
README.md | 39 ++++--
backend/app.py | 282 ++++++++++++++++++++++++++++++++++++++------
frontend/index.html | 151 ++++++++++++++++++++++--
3 files changed, 416 insertions(+), 56 deletions(-)
diff --git a/README.md b/README.md
index 7c3f7cc..0d764e1 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
-
+
@@ -80,7 +80,8 @@ The platform combines security compliance scanning, AI-powered chat with **RAG (
- **AI-powered prompt generation** for OCI infrastructure — generates structured, optimized prompts to feed into the Terraform Agent
- Dedicated chat interface (sub-menu under Terraform) matching Terraform Agent layout
- **Curated model selection**: only 7 recommended models (GPT-4.1, o3, o4-mini, GPT-5.1, GPT-5.2, Gemini 2.5 Pro, Gemini 2.5 Flash), grouped by provider
-- **OCI Terraform Resource Reference**: prompt generation includes the full OCI provider resource catalog as context, ensuring accurate resource names and attributes
+- **Same knowledge pipeline as Terraform Agent**: compact resource reference + auto-detected resource types + official docs from GitHub (cached in SQLite)
+- **Official Terraform docs injection**: detects resource types from user message (keywords + explicit `oci_*` mentions), fetches Example Usage + Argument Reference from GitHub, cached in `tf_resource_docs` table
- **Conversation history**: persistent sessions (agent_type `tf-prompt`) with sidebar history panel, rename/delete, session restore
- **Contextual follow-up**: conversation history sent to GenAI for iterative prompt refinement
- **Copy & Send**: action buttons on assistant messages to copy the prompt or send directly to the Terraform Agent input
@@ -90,6 +91,7 @@ The platform combines security compliance scanning, AI-powered chat with **RAG (
### ⚡ OCI Resource Actions
- **Start/Stop Compute Instances** directly from OCI Account Explorer with one click
- **Start/Stop Autonomous Databases** from Explorer with confirmation prompt
+- **Start/Stop DB Systems, MySQL, Container Instances** — 5 resource types with operational control
- **Update Network Access**: add your IP to ADB ACL (whitelisted_ips) directly from Explorer
- **ACL display**: view current whitelisted IPs for Autonomous Databases in Explorer cards
- **Auto-refresh**: resource state updates automatically after Start/Stop actions (polling)
@@ -97,18 +99,23 @@ The platform combines security compliance scanning, AI-powered chat with **RAG (
- Actions audited in the audit log
### 🔍 OCI Account Explorer
-- **Tree-view navigation** with resizable side panel and drag handle
-- **40+ resource types** across 8 categories:
- - **IAM**: Users, Groups, Policies, Dynamic Groups
- - **Compute**: Instances, Instance Pools, Container Instances
- - **Networking**: VCNs, Subnets, Security Lists, NSGs, Route Tables, NAT/Internet/Service Gateways, Public IPs, Network Firewalls, Firewall Policies
- - **Storage**: Block Volumes, Boot Volumes, Object Storage Buckets, File Systems, Mount Targets
+- **KPI stats bar**: real-time resource counts per category with tooltip breakdown, cached in SQLite for instant loading
+- **Tree-view navigation** with resizable side panel (280px default) and drag handle
+- **36 resource types** across 9 categories:
+ - **Compute**: Instances, Boot Volumes, Instance Pools
+ - **Networking**: VCNs, Subnets, Security Lists, NSGs, Route Tables, NAT/Internet/Service Gateways, Public IPs, Load Balancers
+ - **Storage**: Buckets, Block Volumes, File Systems, Mount Targets
- **Database**: Autonomous Databases, DB Systems, MySQL DB Systems
- - **Kubernetes**: OKE Clusters
- - **Observability**: Alarms, Log Groups, Events Rules, Notification Topics
- - **Security**: Vaults, DNS Zones, API Gateways
+ - **Containers**: Container Instances, OKE Clusters
+ - **Serverless**: Functions, API Gateways
+ - **Observability**: Alarms, Log Groups, Notification Topics, Events Rules
+ - **Security**: Vaults, DNS Zones, Network Firewalls, Firewall Policies
+ - **IAM**: Users, Groups, Policies, Dynamic Groups
+- **Enhanced views**: expandable cards for VCNs (CIDRs, DNS), Subnets (AD, route table), Load Balancers (listeners, backends), Buckets (size, object count, visibility)
- **Multi-region support**: select and filter by multiple OCI regions with checkbox selection
- **Compartment tree**: hierarchical compartment browser with full OCID visibility
+- **Dead state filtering**: automatically hides TERMINATED/DELETED/DELETING resources
+- **Background count refresh**: counts cached in SQLite, refreshed on compartment access without blocking UI
- **Security list detail**: view ingress/egress rules directly in the explorer
- Select which OCI connection to explore
- Real-time API calls via OCI Python SDK
@@ -411,14 +418,14 @@ Allow group to read buckets in compartment
```
oci-cis-agent/
├── backend/
-│ ├── app.py # FastAPI application (~5500 lines)
+│ ├── app.py # FastAPI application (~5900 lines)
│ ├── cis_reports.py # Oracle CIS Benchmark checker (6660 lines, report engine)
│ ├── mcp_cis_server.py # MCP server with 12 granular CIS tools (~700 lines)
│ ├── gen_tf_reference.py # OCI Terraform provider resource catalog generator
│ ├── Dockerfile # Python 3.12 + OCI CLI + Terraform CLI
│ └── requirements.txt # Dependencies
├── frontend/
-│ └── index.html # SPA with Oracle Dark Premium theme (~2950 lines)
+│ └── index.html # SPA with Oracle Dark Premium theme (~3200 lines)
├── nginx/
│ └── default.conf # Reverse proxy config
├── docker-compose.yml # Orchestration
@@ -494,6 +501,8 @@ oci-cis-agent/
| GET | `/api/oci/explore/{id}/log_groups` | List Log Groups |
| GET | `/api/oci/explore/{id}/notification_topics` | List Notification Topics |
| GET | `/api/oci/explore/{id}/events_rules` | List Events Rules |
+| GET | `/api/oci/explore/{id}/counts` | Get cached resource counts (instant from SQLite) |
+| POST | `/api/oci/explore/{id}/counts/refresh` | Refresh all resource counts in background |
### OCI Resource Actions
@@ -501,6 +510,9 @@ oci-cis-agent/
|--------|----------|-------------|
| POST | `/api/oci/instances/{id}/action` | Start/Stop compute instance |
| POST | `/api/oci/autonomous-databases/{id}/action` | Start/Stop Autonomous Database |
+| POST | `/api/oci/db-systems/{id}/action` | Start/Stop DB System |
+| POST | `/api/oci/mysql-db-systems/{id}/action` | Start/Stop MySQL DB System |
+| POST | `/api/oci/container-instances/{id}/action` | Start/Stop Container Instance |
### Generative AI
@@ -685,6 +697,7 @@ All models include OCID mapping for `us-ashburn-1`. For other regions, use the "
| Version | Date | Changes |
|---------|------|---------|
+| **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.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.2** | 2026-03 | **UI Modernization (Phases 1B-5)**: Oracle Dark Premium theme, 45+ Lucide SVG icons replacing all emojis, KPI dashboard with compliance gauge + Chart.js donut/bar charts, report summary API endpoint, Phase 5 animations (staggered fade-in, smooth theme transitions, hover effects) — animations scoped to tab switch only (no flickering on re-render). **Terraform Intelligence**: official resource docs injection from registry.terraform.io (Example Usage + Argument Reference, on-demand fetch + SQLite cache), smart tfvars auto-generation from declared variables, multi-region provider management with `var.region` support, RPC anti-cycle rule, system prompt auto-sync to DB, 60-min polling timeout for long-running applies. **ADB fix**: wallet_password always passed for thin mode mTLS (fixes DPY-6005). CIS_EMBEDDINGS default removed from ADB config |
diff --git a/backend/app.py b/backend/app.py
index 98e47d1..92e3860 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -336,6 +336,16 @@ def init_db():
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
+ CREATE TABLE IF NOT EXISTS explorer_counts_cache (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ oci_config_id TEXT NOT NULL,
+ compartment_id TEXT NOT NULL,
+ resource_type TEXT NOT NULL,
+ count INTEGER NOT NULL DEFAULT 0,
+ regions TEXT DEFAULT '',
+ updated_at TEXT DEFAULT (datetime('now')),
+ UNIQUE(oci_config_id, compartment_id, resource_type, regions)
+ );
CREATE TABLE IF NOT EXISTS tf_valid_types (
name TEXT PRIMARY KEY,
kind TEXT NOT NULL DEFAULT 'resource',
@@ -825,12 +835,89 @@ async def explore_regions(cid: str, u=Depends(current_user)):
except Exception as e:
return {"error": str(e)[:500]}
+_DEAD_STATES = {'TERMINATED','TERMINATING','DELETED','DELETING','PENDING_DELETION','FAULTY'}
+
+_EXP_RESOURCE_TYPES = [
+ 'instances','boot_volumes','instance_pools',
+ 'vcns','subnets','security_lists','nsgs','route_tables','nat_gateways',
+ 'internet_gateways','service_gateways','public_ips','load_balancers',
+ 'buckets','block_volumes','file_systems','mount_targets',
+ 'databases','db_systems','mysql_db_systems',
+ 'container_instances','oke_clusters',
+ 'functions','api_gateways',
+ 'alarms','log_groups','notification_topics','events_rules',
+ 'vaults','dns_zones','network_firewalls','network_firewall_policies',
+ 'iam_users','iam_groups','iam_policies','iam_dynamic_groups',
+]
+
def _explore_comp(cid):
"""Helper: resolve compartment from config."""
with db() as c:
cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone()
return cfg, _safe_dec(cfg["compartment_id"]) or _safe_dec(cfg["tenancy_ocid"])
+# ── Explorer Counts Cache ─────────────────────────────────────────────────────
+@app.get("/api/oci/explore/{cid}/counts")
+async def explore_counts_cached(cid: str, compartment_id: str = Query(...), regions: str = Query(""), u=Depends(current_user)):
+ """Return cached resource counts from DB (instant)."""
+ with db() as c:
+ rows = c.execute(
+ "SELECT resource_type, count, updated_at FROM explorer_counts_cache WHERE oci_config_id=? AND compartment_id=? AND regions=?",
+ (cid, compartment_id, regions)).fetchall()
+ return {r["resource_type"]: {"count": r["count"], "updated_at": r["updated_at"]} for r in rows}
+
+@app.post("/api/oci/explore/{cid}/counts/refresh")
+async def explore_counts_refresh(cid: str, req: dict, u=Depends(current_user)):
+ """Refresh all resource counts in background thread and save to DB."""
+ compartment_id = req.get("compartment_id")
+ regions_list = req.get("regions", [])
+ if not compartment_id:
+ raise HTTPException(400, "compartment_id required")
+ regions_key = ",".join(sorted(regions_list)) if regions_list else ""
+ import threading
+ def _do_refresh():
+ import httpx, asyncio
+ _exp_fn_map = {} # will call explore endpoints via internal dispatch
+ results = {}
+ for rt in _EXP_RESOURCE_TYPES:
+ fn_name = f"explore_{rt}"
+ fn = globals().get(fn_name)
+ if not fn:
+ results[rt] = 0
+ continue
+ try:
+ loop = asyncio.new_event_loop()
+ regs = regions_list if regions_list else [None]
+ total = 0
+ for reg in regs:
+ # IAM endpoints don't take compartment_id/region
+ if rt.startswith('iam_') and rt != 'iam_policies':
+ data = loop.run_until_complete(fn(cid, u=u))
+ elif rt == 'iam_policies':
+ data = loop.run_until_complete(fn(cid, compartment_id=compartment_id, u=u))
+ else:
+ data = loop.run_until_complete(fn(cid, compartment_id=compartment_id, region=reg, u=u))
+ if isinstance(data, list):
+ total += len(data)
+ elif isinstance(data, dict) and not data.get("error"):
+ total += 0
+ results[rt] = total
+ loop.close()
+ except Exception as e:
+ log.warning(f"Explorer count refresh error for {rt}: {e}")
+ results[rt] = 0
+ # Save to DB
+ with db() as c:
+ for rt, cnt in results.items():
+ c.execute("""INSERT INTO explorer_counts_cache (oci_config_id, compartment_id, resource_type, count, regions, updated_at)
+ VALUES (?,?,?,?,?,datetime('now'))
+ ON CONFLICT(oci_config_id, compartment_id, resource_type, regions)
+ DO UPDATE SET count=excluded.count, updated_at=datetime('now')""",
+ (cid, compartment_id, rt, cnt, regions_key))
+ log.info(f"Explorer counts refreshed for config={cid} comp={compartment_id[:20]}... total_types={len(results)}")
+ threading.Thread(target=_do_refresh, daemon=True).start()
+ return {"status": "refreshing", "types": len(_EXP_RESOURCE_TYPES)}
+
@app.get("/api/oci/explore/{cid}/compartment-tree")
async def explore_compartment_tree(cid: str, u=Depends(current_user)):
try:
@@ -884,7 +971,10 @@ async def explore_vcns(cid: str, compartment_id: str = Query(None), region: str
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
vcns = vn.list_vcns(comp).data
- return [{"id":v.id,"display_name":v.display_name,"cidr_blocks":v.cidr_blocks,"lifecycle_state":v.lifecycle_state} for v in vcns]
+ return [{"id":v.id,"display_name":v.display_name,"cidr_blocks":v.cidr_blocks,"lifecycle_state":v.lifecycle_state,
+ "dns_label":v.dns_label,"vcn_domain_name":v.vcn_domain_name,
+ "default_route_table_id":v.default_route_table_id,"default_security_list_id":v.default_security_list_id,
+ "time_created":str(v.time_created)} for v in vcns if v.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -898,7 +988,7 @@ async def explore_instances(cid: str, compartment_id: str = Query(None), region:
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
insts = compute.list_instances(comp).data
- return [{"id":i.id,"display_name":i.display_name,"shape":i.shape,"lifecycle_state":i.lifecycle_state,"region":i.region,"time_created":str(i.time_created)} for i in insts]
+ return [{"id":i.id,"display_name":i.display_name,"shape":i.shape,"lifecycle_state":i.lifecycle_state,"region":i.region,"time_created":str(i.time_created)} for i in insts if i.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -914,7 +1004,7 @@ async def explore_databases(cid: str, compartment_id: str = Query(None), region:
adbs = db_client.list_autonomous_databases(comp).data
return [{"id":a.id,"display_name":a.display_name,"db_name":a.db_name,"lifecycle_state":a.lifecycle_state,
"cpu_core_count":a.cpu_core_count,"data_storage_size_in_tbs":a.data_storage_size_in_tbs,
- "is_free_tier":a.is_free_tier,"whitelisted_ips":a.whitelisted_ips or []} for a in adbs]
+ "is_free_tier":a.is_free_tier,"whitelisted_ips":a.whitelisted_ips or []} for a in adbs if a.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -928,8 +1018,11 @@ async def explore_buckets(cid: str, compartment_id: str = Query(None), region: s
namespace = os_client.get_namespace().data
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
- buckets = os_client.list_buckets(namespace, comp).data
- return [{"name":b.name,"namespace":b.namespace,"time_created":str(b.time_created)} for b in buckets]
+ buckets = os_client.list_buckets(namespace, comp, fields=["approximateSize","approximateCount"]).data
+ return [{"name":b.name,"namespace":b.namespace,"time_created":str(b.time_created),
+ "approximate_size":b.approximate_size,"approximate_count":b.approximate_count,
+ "public_access_type":b.public_access_type or "NoPublicAccess",
+ "storage_tier":b.storage_tier,"versioning":b.versioning} for b in buckets]
except Exception as e:
return {"error": str(e)[:500]}
@@ -944,7 +1037,10 @@ async def explore_subnets(cid: str, compartment_id: str = Query(None), region: s
comp = compartment_id or default_comp
subs = vn.list_subnets(comp).data
return [{"id":s.id,"display_name":s.display_name,"cidr_block":s.cidr_block,"vcn_id":s.vcn_id,
- "lifecycle_state":s.lifecycle_state,"prohibit_public_ip_on_vnic":s.prohibit_public_ip_on_vnic} for s in subs]
+ "lifecycle_state":s.lifecycle_state,"prohibit_public_ip_on_vnic":s.prohibit_public_ip_on_vnic,
+ "availability_domain":s.availability_domain or "Regional","dns_label":s.dns_label,
+ "route_table_id":s.route_table_id,"security_list_ids":s.security_list_ids,
+ "subnet_domain_name":s.subnet_domain_name,"time_created":str(s.time_created)} for s in subs if s.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -973,6 +1069,7 @@ async def explore_security_lists(cid: str, compartment_id: str = Query(None), re
"stateless":r.is_stateless,"description":getattr(r,'description','') or "","rule_index":idx}
result = []
for s in sls:
+ if s.lifecycle_state in _DEAD_STATES: continue
ingress = [_fmt_rule(r,"ingress",i) for i,r in enumerate(s.ingress_security_rules or [])]
egress = [_fmt_rule(r,"egress",i) for i,r in enumerate(s.egress_security_rules or [])]
result.append({"id":s.id,"display_name":s.display_name,"vcn_id":s.vcn_id,"lifecycle_state":s.lifecycle_state,
@@ -1073,7 +1170,7 @@ async def explore_block_volumes(cid: str, compartment_id: str = Query(None), reg
comp = compartment_id or default_comp
vols = bs.list_volumes(compartment_id=comp).data
return [{"id":v.id,"display_name":v.display_name,"size_in_gbs":v.size_in_gbs,
- "lifecycle_state":v.lifecycle_state,"vpus_per_gb":v.vpus_per_gb} for v in vols]
+ "lifecycle_state":v.lifecycle_state,"vpus_per_gb":v.vpus_per_gb} for v in vols if v.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -1087,8 +1184,21 @@ async def explore_load_balancers(cid: str, compartment_id: str = Query(None), re
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
lbs = lb.list_load_balancers(comp).data
- return [{"id":l.id,"display_name":l.display_name,"lifecycle_state":l.lifecycle_state,
- "shape_name":l.shape_name,"ip_addresses":[ip.ip_address for ip in (l.ip_addresses or [])]} for l in lbs]
+ result = []
+ for l in lbs:
+ if l.lifecycle_state in _DEAD_STATES: continue
+ listeners = []
+ for name, lis in (l.listeners or {}).items():
+ listeners.append({"name": name, "port": lis.port, "protocol": lis.protocol,
+ "backend_set": lis.default_backend_set_name})
+ backend_sets = []
+ for name, bs in (l.backend_sets or {}).items():
+ backends = [{"ip": b.ip_address, "port": b.port, "weight": b.weight} for b in (bs.backends or [])]
+ backend_sets.append({"name": name, "backends": backends, "policy": bs.policy})
+ result.append({"id":l.id,"display_name":l.display_name,"lifecycle_state":l.lifecycle_state,
+ "shape_name":l.shape_name,"ip_addresses":[ip.ip_address for ip in (l.ip_addresses or [])],
+ "is_private":l.is_private,"listeners":listeners,"backend_sets":backend_sets})
+ return result
except Exception as e:
return {"error": str(e)[:500]}
@@ -1104,8 +1214,10 @@ async def explore_functions(cid: str, compartment_id: str = Query(None), region:
apps = fn.list_applications(comp).data
result = []
for a in apps:
+ if a.lifecycle_state in _DEAD_STATES: continue
fns = fn.list_functions(a.id).data
for f in fns:
+ if f.lifecycle_state in _DEAD_STATES: continue
result.append({"id":f.id,"display_name":f.display_name,"application":a.display_name,
"lifecycle_state":f.lifecycle_state,"memory_in_mbs":f.memory_in_mbs,
"timeout_in_seconds":f.timeout_in_seconds,"image":f.image})
@@ -1125,7 +1237,7 @@ async def explore_container_instances(cid: str, compartment_id: str = Query(None
cis = ci.list_container_instances(comp).data
return [{"id":c.id,"display_name":c.display_name,"lifecycle_state":c.lifecycle_state,
"container_count":c.container_count,"shape":c.shape,
- "time_created":str(c.time_created)} for c in cis]
+ "time_created":str(c.time_created)} for c in cis if c.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -1142,6 +1254,7 @@ async def explore_nsgs(cid: str, compartment_id: str = Query(None), region: str
result = []
proto_map = {"1": "ICMP", "6": "TCP", "17": "UDP", "all": "ALL"}
for n in nsgs:
+ if n.lifecycle_state in _DEAD_STATES: continue
rules_data = oci.pagination.list_call_get_all_results(
vn.list_network_security_group_security_rules, n.id).data
rules = []
@@ -1242,6 +1355,7 @@ async def explore_route_tables(cid: str, compartment_id: str = Query(None), regi
rts = vn.list_route_tables(comp).data
result = []
for rt in rts:
+ if rt.lifecycle_state in _DEAD_STATES: continue
routes = []
for r in (rt.route_rules or []):
target_type = r.network_entity_id.split(".")[1] if r.network_entity_id and "." in r.network_entity_id else "unknown"
@@ -1265,7 +1379,7 @@ async def explore_nat_gateways(cid: str, compartment_id: str = Query(None), regi
comp = compartment_id or default_comp
ngs = vn.list_nat_gateways(comp).data
return [{"id":n.id,"display_name":n.display_name,"vcn_id":n.vcn_id,"lifecycle_state":n.lifecycle_state,
- "nat_ip":n.nat_ip,"block_traffic":n.block_traffic} for n in ngs]
+ "nat_ip":n.nat_ip,"block_traffic":n.block_traffic} for n in ngs if n.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -1280,7 +1394,7 @@ async def explore_internet_gateways(cid: str, compartment_id: str = Query(None),
comp = compartment_id or default_comp
igs = vn.list_internet_gateways(comp).data
return [{"id":g.id,"display_name":g.display_name,"vcn_id":g.vcn_id,"lifecycle_state":g.lifecycle_state,
- "is_enabled":g.is_enabled} for g in igs]
+ "is_enabled":g.is_enabled} for g in igs if g.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -1295,7 +1409,7 @@ async def explore_service_gateways(cid: str, compartment_id: str = Query(None),
comp = compartment_id or default_comp
sgs = vn.list_service_gateways(comp).data
return [{"id":g.id,"display_name":g.display_name,"vcn_id":g.vcn_id,"lifecycle_state":g.lifecycle_state,
- "services":[s.service_name for s in (g.services or [])]} for g in sgs]
+ "services":[s.service_name for s in (g.services or [])]} for g in sgs if g.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -1311,7 +1425,7 @@ async def explore_public_ips(cid: str, compartment_id: str = Query(None), region
ips = vn.list_public_ips(scope="REGION", compartment_id=comp).data
return [{"id":ip.id,"display_name":ip.display_name or "—","ip_address":ip.ip_address,
"lifecycle_state":ip.lifecycle_state,"lifetime":ip.lifetime,
- "assigned_entity_type":ip.assigned_entity_type} for ip in ips]
+ "assigned_entity_type":ip.assigned_entity_type} for ip in ips if ip.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -1330,6 +1444,7 @@ async def explore_boot_volumes(cid: str, compartment_id: str = Query(None), regi
for ad in ads:
bvs = bs.list_boot_volumes(availability_domain=ad.name, compartment_id=comp).data
for v in bvs:
+ if v.lifecycle_state in _DEAD_STATES: continue
result.append({"id":v.id,"display_name":v.display_name,"size_in_gbs":v.size_in_gbs,
"lifecycle_state":v.lifecycle_state,"availability_domain":ad.name,
"vpus_per_gb":v.vpus_per_gb})
@@ -1348,7 +1463,7 @@ async def explore_instance_pools(cid: str, compartment_id: str = Query(None), re
comp = compartment_id or default_comp
pools = cm.list_instance_pools(comp).data
return [{"id":p.id,"display_name":p.display_name,"lifecycle_state":p.lifecycle_state,
- "size":p.size,"time_created":str(p.time_created)} for p in pools]
+ "size":p.size,"time_created":str(p.time_created)} for p in pools if p.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -1364,7 +1479,7 @@ async def explore_oke_clusters(cid: str, compartment_id: str = Query(None), regi
clusters = ce.list_clusters(comp).data
return [{"id":c.id,"name":c.name,"lifecycle_state":c.lifecycle_state,
"kubernetes_version":c.kubernetes_version,"vcn_id":c.vcn_id,
- "endpoint_config":c.endpoint_config.is_public_ip_enabled if c.endpoint_config else None} for c in clusters]
+ "endpoint_config":c.endpoint_config.is_public_ip_enabled if c.endpoint_config else None} for c in clusters if c.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -1383,6 +1498,7 @@ async def explore_file_systems(cid: str, compartment_id: str = Query(None), regi
for ad in ads:
fs_list = fss.list_file_systems(comp, ad.name).data
for f in fs_list:
+ if f.lifecycle_state in _DEAD_STATES: continue
result.append({"id":f.id,"display_name":f.display_name,"lifecycle_state":f.lifecycle_state,
"availability_domain":ad.name,"metered_bytes":f.metered_bytes,
"time_created":str(f.time_created)})
@@ -1405,6 +1521,7 @@ async def explore_mount_targets(cid: str, compartment_id: str = Query(None), reg
for ad in ads:
mts = fss.list_mount_targets(comp, ad.name).data
for m in mts:
+ if m.lifecycle_state in _DEAD_STATES: continue
result.append({"id":m.id,"display_name":m.display_name,"lifecycle_state":m.lifecycle_state,
"availability_domain":ad.name,"subnet_id":m.subnet_id,
"private_ip_ids":m.private_ip_ids})
@@ -1424,7 +1541,7 @@ async def explore_db_systems(cid: str, compartment_id: str = Query(None), region
dbs = db_client.list_db_systems(comp).data
return [{"id":d.id,"display_name":d.display_name,"lifecycle_state":d.lifecycle_state,
"shape":d.shape,"database_edition":d.database_edition,
- "cpu_core_count":d.cpu_core_count,"node_count":d.node_count} for d in dbs]
+ "cpu_core_count":d.cpu_core_count,"node_count":d.node_count} for d in dbs if d.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -1440,7 +1557,7 @@ async def explore_mysql_db_systems(cid: str, compartment_id: str = Query(None),
dbs = mysql.list_db_systems(comp).data
return [{"id":d.id,"display_name":d.display_name,"lifecycle_state":d.lifecycle_state,
"shape_name":d.shape_name,"mysql_version":d.mysql_version,
- "is_highly_available":d.is_highly_available} for d in dbs]
+ "is_highly_available":d.is_highly_available} for d in dbs if d.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -1455,7 +1572,7 @@ async def explore_alarms(cid: str, compartment_id: str = Query(None), region: st
comp = compartment_id or default_comp
alarms = mon.list_alarms(comp).data
return [{"id":a.id,"display_name":a.display_name,"lifecycle_state":a.lifecycle_state,
- "severity":a.severity,"namespace":a.namespace,"is_enabled":a.is_enabled} for a in alarms]
+ "severity":a.severity,"namespace":a.namespace,"is_enabled":a.is_enabled} for a in alarms if a.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -1485,7 +1602,7 @@ async def explore_iam_users(cid: str, u=Depends(current_user)):
users = identity.list_users(tenancy).data
return [{"id":u2.id,"name":u2.name,"email":u2.email or "","lifecycle_state":u2.lifecycle_state,
"is_mfa_activated":u2.is_mfa_activated,"time_created":str(u2.time_created),
- "last_successful_login_time":str(u2.last_successful_login_time) if u2.last_successful_login_time else None} for u2 in users]
+ "last_successful_login_time":str(u2.last_successful_login_time) if u2.last_successful_login_time else None} for u2 in users if u2.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -1499,7 +1616,7 @@ async def explore_iam_groups(cid: str, u=Depends(current_user)):
tenancy = _safe_dec(cfg["tenancy_ocid"])
groups = identity.list_groups(tenancy).data
return [{"id":g.id,"name":g.name,"description":g.description or "","lifecycle_state":g.lifecycle_state,
- "time_created":str(g.time_created)} for g in groups]
+ "time_created":str(g.time_created)} for g in groups if g.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -1513,7 +1630,7 @@ async def explore_iam_policies(cid: str, compartment_id: str = Query(None), u=De
comp = compartment_id or _safe_dec(cfg["tenancy_ocid"]) or default_comp
policies = identity.list_policies(comp).data
return [{"id":p.id,"name":p.name,"description":p.description or "","lifecycle_state":p.lifecycle_state,
- "statements":p.statements,"time_created":str(p.time_created)} for p in policies]
+ "statements":p.statements,"time_created":str(p.time_created)} for p in policies if p.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -1527,7 +1644,7 @@ async def explore_iam_dynamic_groups(cid: str, u=Depends(current_user)):
tenancy = _safe_dec(cfg["tenancy_ocid"])
dgs = identity.list_dynamic_groups(tenancy).data
return [{"id":g.id,"name":g.name,"description":g.description or "","lifecycle_state":g.lifecycle_state,
- "matching_rule":g.matching_rule,"time_created":str(g.time_created)} for g in dgs]
+ "matching_rule":g.matching_rule,"time_created":str(g.time_created)} for g in dgs if g.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -1543,7 +1660,7 @@ async def explore_vaults(cid: str, compartment_id: str = Query(None), region: st
vaults = kms.list_vaults(comp).data
return [{"id":v.id,"display_name":v.display_name,"lifecycle_state":v.lifecycle_state,
"vault_type":v.vault_type,"crypto_endpoint":v.crypto_endpoint,
- "time_created":str(v.time_created)} for v in vaults]
+ "time_created":str(v.time_created)} for v in vaults if v.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -1559,7 +1676,7 @@ async def explore_dns_zones(cid: str, compartment_id: str = Query(None), region:
zones = dns.list_zones(comp).data
return [{"id":z.id,"name":z.name,"lifecycle_state":z.lifecycle_state,
"zone_type":z.zone_type,"serial":z.serial,
- "time_created":str(z.time_created)} for z in zones]
+ "time_created":str(z.time_created)} for z in zones if z.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -1575,7 +1692,7 @@ async def explore_api_gateways(cid: str, compartment_id: str = Query(None), regi
gws = apigw.list_gateways(comp).data
return [{"id":g.id,"display_name":g.display_name,"lifecycle_state":g.lifecycle_state,
"endpoint_type":g.endpoint_type,"hostname":g.hostname,
- "subnet_id":g.subnet_id,"time_created":str(g.time_created)} for g in gws]
+ "subnet_id":g.subnet_id,"time_created":str(g.time_created)} for g in gws if g.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -1591,7 +1708,7 @@ async def explore_notification_topics(cid: str, compartment_id: str = Query(None
topics = ons.list_topics(comp).data
return [{"id":t.topic_id,"name":t.name,"lifecycle_state":t.lifecycle_state,
"description":t.description or "","api_endpoint":t.api_endpoint,
- "time_created":str(t.time_created)} for t in topics]
+ "time_created":str(t.time_created)} for t in topics if t.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -1607,7 +1724,7 @@ async def explore_events_rules(cid: str, compartment_id: str = Query(None), regi
rules = events.list_rules(comp).data
return [{"id":r.id,"display_name":r.display_name,"lifecycle_state":r.lifecycle_state,
"condition":r.condition,"is_enabled":r.is_enabled,
- "description":r.description or "","time_created":str(r.time_created)} for r in rules]
+ "description":r.description or "","time_created":str(r.time_created)} for r in rules if r.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -1620,10 +1737,11 @@ async def explore_network_firewalls(cid: str, compartment_id: str = Query(None),
fw = oci.network_firewall.NetworkFirewallClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
- firewalls = fw.list_network_firewalls(compartment_id=comp).data
+ resp = fw.list_network_firewalls(compartment_id=comp).data
+ firewalls = resp.items if hasattr(resp, 'items') else (resp if isinstance(resp, list) else [])
return [{"id":f.id,"display_name":f.display_name,"lifecycle_state":f.lifecycle_state,
"subnet_id":f.subnet_id,"network_firewall_policy_id":f.network_firewall_policy_id,
- "time_created":str(f.time_created)} for f in firewalls]
+ "time_created":str(f.time_created)} for f in firewalls if f.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -1636,9 +1754,10 @@ async def explore_network_firewall_policies(cid: str, compartment_id: str = Quer
fw = oci.network_firewall.NetworkFirewallClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
- policies = fw.list_network_firewall_policies(compartment_id=comp).data
+ resp = fw.list_network_firewall_policies(compartment_id=comp).data
+ policies = resp.items if hasattr(resp, 'items') else (resp if isinstance(resp, list) else [])
return [{"id":p.id,"display_name":p.display_name,"lifecycle_state":p.lifecycle_state,
- "time_created":str(p.time_created)} for p in policies]
+ "time_created":str(p.time_created)} for p in policies if p.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@@ -4574,6 +4693,79 @@ async def oci_adb_update_network(adb_id: str, req: dict, u=Depends(current_user)
raise HTTPException(500, str(e)[:500])
+@app.post("/api/oci/db-systems/{db_system_id}/action")
+async def oci_db_system_action(db_system_id: str, req: dict, u=Depends(current_user)):
+ action = req.get("action", "").upper()
+ oci_config_id = req.get("oci_config_id", "")
+ region = req.get("region")
+ if action not in ("START", "STOP"):
+ raise HTTPException(400, "Ação inválida. Use START ou STOP.")
+ if not oci_config_id:
+ raise HTTPException(400, "oci_config_id obrigatório")
+ try:
+ import oci
+ config = _get_oci_config(oci_config_id)
+ if region: config["region"] = region
+ db_client = oci.database.DatabaseClient(config)
+ if action == "START":
+ db_client.start_db_system(db_system_id)
+ else:
+ db_client.stop_db_system(db_system_id)
+ _audit(u["id"], u["username"], f"db_system_{action.lower()}", db_system_id)
+ return {"ok": True, "action": action, "db_system_id": db_system_id}
+ except Exception as e:
+ raise HTTPException(500, str(e)[:500])
+
+
+@app.post("/api/oci/mysql-db-systems/{db_system_id}/action")
+async def oci_mysql_action(db_system_id: str, req: dict, u=Depends(current_user)):
+ action = req.get("action", "").lower()
+ oci_config_id = req.get("oci_config_id", "")
+ region = req.get("region")
+ if action not in ("start", "stop"):
+ raise HTTPException(400, "Ação inválida. Use start ou stop.")
+ if not oci_config_id:
+ raise HTTPException(400, "oci_config_id obrigatório")
+ try:
+ import oci
+ config = _get_oci_config(oci_config_id)
+ if region: config["region"] = region
+ mysql = oci.mysql.DbSystemClient(config)
+ if action == "start":
+ mysql.start_db_system(db_system_id)
+ else:
+ shutdown_type = req.get("shutdown_type", "SLOW")
+ mysql.stop_db_system(db_system_id, oci.mysql.models.StopDbSystemDetails(shutdown_type=shutdown_type))
+ _audit(u["id"], u["username"], f"mysql_{action}", db_system_id)
+ return {"ok": True, "action": action, "db_system_id": db_system_id}
+ except Exception as e:
+ raise HTTPException(500, str(e)[:500])
+
+
+@app.post("/api/oci/container-instances/{ci_id}/action")
+async def oci_container_instance_action(ci_id: str, req: dict, u=Depends(current_user)):
+ action = req.get("action", "").lower()
+ oci_config_id = req.get("oci_config_id", "")
+ region = req.get("region")
+ if action not in ("start", "stop"):
+ raise HTTPException(400, "Ação inválida. Use start ou stop.")
+ if not oci_config_id:
+ raise HTTPException(400, "oci_config_id obrigatório")
+ try:
+ import oci
+ config = _get_oci_config(oci_config_id)
+ if region: config["region"] = region
+ ci = oci.container_instances.ContainerInstanceClient(config)
+ if action == "start":
+ ci.start_container_instance(ci_id)
+ else:
+ ci.stop_container_instance(ci_id)
+ _audit(u["id"], u["username"], f"container_instance_{action}", ci_id)
+ return {"ok": True, "action": action, "container_instance_id": ci_id}
+ except Exception as e:
+ raise HTTPException(500, str(e)[:500])
+
+
@app.get("/api/oci/my-ip")
async def get_my_ip(request: Request):
"""Return the caller's public IP address."""
@@ -4684,11 +4876,33 @@ async def tf_generate_prompt(req: TfPromptReq, u=Depends(current_user)):
(sid, u["id"], "tf-prompt", title))
else:
c.execute("UPDATE chat_sessions SET updated_at=datetime('now') WHERE id=?", (sid,))
- # Load TF resource reference for context
+ # Load TF resource reference (compact — same as Terraform Agent)
tf_ref = _load_tf_resource_reference()
system_with_ref = TFP_SYSTEM_PROMPT
if tf_ref:
- system_with_ref += f"\n\n### Referência de Recursos OCI Terraform Disponíveis\nUse esta referência para garantir que os recursos mencionados no prompt existam no provider OCI:\n\n{tf_ref}"
+ sections = []
+ for line in tf_ref.split('\n'):
+ if line.startswith('## All Resource Types'):
+ break
+ sections.append(line)
+ ref_compact = '\n'.join(sections).strip()
+ if ref_compact:
+ system_with_ref += "\n\n### Referência de Recursos OCI Terraform (gerado do provider schema)\n" + \
+ "Use EXATAMENTE estes nomes de resource types. Se o recurso não estiver nesta lista, ele NÃO EXISTE no provider.\n\n" + \
+ ref_compact
+
+ # Detect resource types from user message + history and fetch official docs
+ detect_text = req.message
+ if req.history:
+ for h in req.history[-3:]:
+ detect_text += "\n" + (h.get("content", "") or "")
+ resource_types = _detect_tf_resource_types(detect_text)
+ if resource_types:
+ docs_ctx = _get_tf_docs_for_resources(resource_types)
+ if docs_ctx:
+ system_with_ref += "\n\n" + docs_ctx
+ log.info(f"TF Prompt Generator: injected {len(resource_types)} resource docs ({len(docs_ctx)} chars)")
+
gc["system_prompt"] = system_with_ref
gc["temperature"] = 0.7
gc["max_tokens"] = 4000
diff --git a/frontend/index.html b/frontend/index.html
index 5f93530..b374e73 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -244,7 +244,7 @@ tbody tr:last-child td{border-bottom:none}
/* ── Explorer ── */
.exp-wrap{display:flex;height:calc(100vh - 56px - 3rem);border:1px solid var(--bd);border-radius:var(--rl);overflow:hidden;box-shadow:var(--sh2)}
-.exp-tree{width:240px;flex-shrink:0;background:var(--bg1);border-right:none;overflow-y:auto;padding:.5rem 0;min-width:140px;max-width:50%}
+.exp-tree{width:280px;flex-shrink:0;background:var(--bg1);border-right:none;overflow-y:auto;padding:0;min-width:180px;max-width:50%}
.exp-resize{width:5px;flex-shrink:0;cursor:col-resize;background:var(--bd);transition:background .15s;position:relative}
.exp-resize:hover,.exp-resize.dragging{background:var(--ac);opacity:.6}
.exp-resize::after{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:3px;height:28px;border-left:1px solid var(--t4);border-right:1px solid var(--t4);opacity:.4}
@@ -268,7 +268,7 @@ tbody tr:last-child td{border-bottom:none}
.exp-c strong{color:var(--t1);font-size:.84rem;font-weight:600;display:block;overflow:hidden;text-overflow:ellipsis}
.exp-c .em{font-family:var(--fm);font-size:.65rem;color:var(--t4);word-break:break-all;overflow-wrap:break-word;display:inline-block;max-width:100%}
.exp-node{padding:.15rem 0;font-size:.74rem;color:var(--t2)}
-.exp-node-row{display:flex;align-items:center;gap:.25rem;padding:.25rem .5rem;border-radius:6px;cursor:pointer;transition:all .15s}
+.exp-node-row{display:flex;align-items:center;gap:.3rem;padding:.35rem .6rem;border-radius:6px;cursor:pointer;transition:all .15s;font-size:.76rem}
.exp-node-row:hover{background:var(--bg2)}
.exp-node-row.selected{background:var(--acl);color:var(--ac);font-weight:600;border-left:2px solid var(--ac)}
.exp-node-toggle{width:16px;text-align:center;font-size:.55rem;color:var(--t4);flex-shrink:0;cursor:pointer;transition:transform .15s}
@@ -531,7 +531,7 @@ const IC={
const LOGO_W=``;
const LOGO_R=``;
-const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],models:{},regions:[],embModels:{},expData:null,expCfg:'',expSelRegions:[],expRegDdOpen:false,expTree:[],expSelComp:'',expCat:'Compute',expResType:'instances',expTreeOpen:{},expLoading:false,expRegions:[],expCounts:{},expTreeW:null,editing:null,
+const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],models:{},regions:[],embModels:{},expData:null,expCfg:'',expSelRegions:[],expRegDdOpen:false,expTree:[],expSelComp:'',expCat:'Compute',expResType:'instances',expTreeOpen:{},expLoading:false,expRegions:[],expCounts:{},expCountsRefreshing:false,expTreeW:null,editing:null,
chatModel:'',chatOci:'',chatRegion:'',chatCompartment:'',chatHistOpen:false,chatHistory:[],tfHistOpen:false,tfHistory:[],
chatParams:{temperature:1,max_tokens:6000,top_p:0.95,top_k:1,frequency_penalty:0,presence_penalty:0,reasoning_effort:'medium'},chatPanel:'',chatUseTools:true,
chatPrompts:[],editingPrompt:null,trackingReportId:null,ociRegions:{},rptSelRegions:[],rptRegionsOpen:false,rptRegionFilter:'',
@@ -1615,6 +1615,21 @@ async function ociAdbAction(id,action){
if(!confirm(label+' este Autonomous Database?'))return;
try{await $api('/oci/autonomous-databases/'+id+'/action',{method:'POST',body:{action,oci_config_id:S.expCfg,region:null}});expPollState(action==='start'?'AVAILABLE':'STOPPED')}catch(e){alert('Erro: '+e.message)}
}
+async function ociDbSystemAction(id,action){
+ const label=action==='START'?'Iniciar':'Parar';
+ if(!confirm(label+' este DB System?'))return;
+ try{await $api('/oci/db-systems/'+id+'/action',{method:'POST',body:{action,oci_config_id:S.expCfg,region:null}});expPollState(action==='START'?'AVAILABLE':'STOPPED')}catch(e){alert('Erro: '+e.message)}
+}
+async function ociMysqlAction(id,action){
+ const label=action==='start'?'Iniciar':'Parar';
+ if(!confirm(label+' este MySQL DB System?'))return;
+ try{await $api('/oci/mysql-db-systems/'+id+'/action',{method:'POST',body:{action,oci_config_id:S.expCfg,region:null}});expPollState(action==='start'?'ACTIVE':'INACTIVE')}catch(e){alert('Erro: '+e.message)}
+}
+async function ociContainerAction(id,action){
+ const label=action==='start'?'Iniciar':'Parar';
+ if(!confirm(label+' esta Container Instance?'))return;
+ try{await $api('/oci/container-instances/'+id+'/action',{method:'POST',body:{action,oci_config_id:S.expCfg,region:null}});expPollState(action==='start'?'ACTIVE':'INACTIVE')}catch(e){alert('Erro: '+e.message)}
+}
async function ociAdbUpdateNet(id){
try{
const ipResp=await $api('/oci/my-ip');
@@ -1844,8 +1859,19 @@ function rExplorer(){
const cats=EXP_GROUPS.map(g=>`${catIcons[g.name]||''} ${g.name}
`).join('');
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`${t.label}${cnt!=null?' ('+cnt+')':''}
`}).join('');
+ // KPI stats from counts
+ 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:detail}}).filter(k=>k.count>0);
+ const selCompName=S.expTree.find(n=>n.id===S.expSelComp);
+ const compLabel=selCompName?selCompName.name:'—';
+ const kpiHtml=S.expSelComp?`
+
${IC.folder} Compartment
${compLabel}
${S.expSelRegions.length||'Todas'} região(ões)
+
${IC.activity} Total
${totalRes}
${S.expCountsRefreshing?' atualizando...':'recursos'}
+${kpiCats.slice(0,6).map(k=>`
${k.icon}
${k.count}
${k.name}
`).join('')}
+
`:'';
return`
-
${IC.folder} Compartments
${S.expTree.length?rExpTree(null,0):'
Carregando...
'}
+
${IC.folder} Compartments
${S.expTree.length?rExpTree(null,0):'
Carregando...
'}
+${kpiHtml}
${tabs}
${S.expLoading?'
Consultando OCI...
':renderExpData(S.expData)}
@@ -1873,19 +1900,19 @@ function rExpTree(parentId,depth){
${hasKids?(isOpen?'▼':'▶'):' '}
${n.name}
${hasKids&&isOpen?`
${rExpTree(n.id,depth+1)}
`:''}
`}).join('')}
-async function expSetCfg(cid){S.expCfg=cid;S.expTree=[];S.expSelComp='';S.expData=null;S.expSelRegions=[];S.expRegions=[];S.expCounts={};S.expRegDdOpen=false;R();await expLoadTree();await expLoadRegions()}
+async function expSetCfg(cid){S.expCfg=cid;S.expTree=[];S.expSelComp='';S.expData=null;S.expSelRegions=[];S.expRegions=[];S.expCounts={};S.expRegDdOpen=false;_expCountAbort++;R();await expLoadTree();await expLoadRegions()}
function expToggleRegion(r){const i=S.expSelRegions.indexOf(r);if(i>=0)S.expSelRegions.splice(i,1);else S.expSelRegions.push(r);R()}
function expRemoveRegion(r){S.expSelRegions=S.expSelRegions.filter(x=>x!==r);if(S.expSelComp)expLoadRes()}
-async function expApplyRegions(){if(S.expSelComp)await expLoadRes()}
+async function expApplyRegions(){if(S.expSelComp){S.expCounts={};_expCountAbort++;await expLoadRes();expLoadCachedCounts();expRefreshCounts()}}
function expSetCat(c){S.expCat=c;const grp=EXP_GROUPS.find(g=>g.name===c);if(grp&&grp.tabs.length){S.expResType=grp.tabs[0].key;S.expData=null;R();if(S.expSelComp)expLoadRes()}else R()}
function expSetTab(t){S.expResType=t;S.expData=null;R();if(S.expSelComp)expLoadRes()}
function expToggleNode(id){S.expTreeOpen[id]=!S.expTreeOpen[id];R()}
-async function expSelectComp(id){S.expSelComp=id;S.expCounts={};await expLoadRes()}
+async function expSelectComp(id){S.expSelComp=id;S.expCounts={};_expCountAbort++;expLoadCachedCounts();await expLoadRes();expRefreshCounts()}
function expStartResize(e){e.preventDefault();const tree=document.getElementById('exp-tree');const handle=e.target;if(!tree)return;handle.classList.add('dragging');const startX=e.clientX;const startW=tree.offsetWidth;
const onMove=ev=>{const w=Math.max(140,Math.min(startW+(ev.clientX-startX),window.innerWidth*0.5));tree.style.width=w+'px';S.expTreeW=w};
const onUp=()=>{handle.classList.remove('dragging');document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);document.body.style.cursor='';document.body.style.userSelect=''};
document.body.style.cursor='col-resize';document.body.style.userSelect='none';document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp)}
-async function expRefresh(){if(S.expCfg){S.expTree=[];S.expCounts={};R();await expLoadTree();if(S.expSelComp)await expLoadRes()}}
+async function expRefresh(){if(S.expCfg){S.expTree=[];S.expCounts={};_expCountAbort++;R();await expLoadTree();if(S.expSelComp){await expLoadRes();expRefreshCounts()}}}
async function expLoadTree(){
if(!S.expCfg)return;
try{const d=await $api('/oci/explore/'+S.expCfg+'/compartment-tree');
@@ -1917,25 +1944,131 @@ async function expLoadRes(){
else S.expData=[];
S.expCounts[S.expResType]=all.length;
S.expLoading=false;R()}
+let _expCountAbort=0;
+async function expLoadCachedCounts(){
+ const comp=S.expSelComp;const cfg=S.expCfg;if(!comp||!cfg)return;
+ const regKey=S.expSelRegions.length?S.expSelRegions.slice().sort().join(','):'';
+ try{
+ const q=new URLSearchParams({compartment_id:comp,regions:regKey});
+ 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()}
+ }catch(e){console.warn('Cache counts error:',e)}
+}
+async function expRefreshCounts(){
+ const comp=S.expSelComp;const cfg=S.expCfg;if(!comp||!cfg)return;
+ const myRun=++_expCountAbort;
+ S.expCountsRefreshing=true;R();
+ try{
+ await $api('/oci/explore/'+cfg+'/counts/refresh',{method:'POST',body:{compartment_id:comp,regions:S.expSelRegions}});
+ const poll=async(attempts)=>{
+ if(_expCountAbort!==myRun||S.expSelComp!==comp){S.expCountsRefreshing=false;R();return}
+ if(attempts<=0){S.expCountsRefreshing=false;R();return}
+ await new Promise(r=>setTimeout(r,3000));
+ if(_expCountAbort!==myRun||S.expSelComp!==comp){S.expCountsRefreshing=false;R();return}
+ await expLoadCachedCounts();
+ poll(attempts-1);
+ };
+ poll(12);
+ }catch(e){S.expCountsRefreshing=false;R();console.warn('Refresh counts error:',e)}
+}
function renderExpData(d){if(!d)return'Selecione um compartment na árvore.
';
if(d.error)return`${d.error}
`;if(!Array.isArray(d)||!d.length)return'Nenhum recurso encontrado neste compartment.
';
if(S.expResType==='security_lists')return rExpSecLists(d);
if(S.expResType==='nsgs')return rExpNsgs(d);
if(S.expResType==='route_tables')return rExpRouteTables(d);
+ if(S.expResType==='vcns')return rExpVcns(d);
+ if(S.expResType==='subnets')return rExpSubnets(d);
+ if(S.expResType==='load_balancers')return rExpLoadBalancers(d);
+ if(S.expResType==='buckets')return rExpBuckets(d);
const isInst=S.expResType==='instances';
const isAdb=S.expResType==='databases';
+ const isDbSys=S.expResType==='db_systems';
+ const isMysql=S.expResType==='mysql_db_systems';
+ const isCI=S.expResType==='container_instances';
return`${d.length} item(ns) encontrado(s)
${d.map(i=>{
const keys=Object.keys(i);
const name=i.display_name||i.name||i.db_name||i.application||'—';
- const skip=['display_name','name','db_name','lifecycle_state','application','rules','routes','whitelisted_ips'];
+ const skip=['display_name','name','db_name','lifecycle_state','application','rules','routes','whitelisted_ips','listeners','backend_sets'];
const aclHtml=isAdb&&i.whitelisted_ips&&i.whitelisted_ips.length?'
ACL: '+i.whitelisted_ips.join(', ')+'':'';
let actionBtn='';
if(isInst&&i.lifecycle_state==='RUNNING')actionBtn='
';
else if(isInst&&i.lifecycle_state==='STOPPED')actionBtn='
';
else if(isAdb&&i.lifecycle_state==='AVAILABLE')actionBtn='
';
else if(isAdb&&i.lifecycle_state==='STOPPED')actionBtn='
';
+ else if(isDbSys&&i.lifecycle_state==='AVAILABLE')actionBtn='
';
+ else if(isDbSys&&i.lifecycle_state==='STOPPED')actionBtn='
';
+ else if(isMysql&&i.lifecycle_state==='ACTIVE')actionBtn='
';
+ else if(isMysql&&i.lifecycle_state==='INACTIVE')actionBtn='
';
+ else if(isCI&&i.lifecycle_state==='ACTIVE')actionBtn='
';
+ else if(isCI&&i.lifecycle_state==='INACTIVE')actionBtn='
';
return`
${name}${i.lifecycle_state?` ${i.lifecycle_state}`:''}${actionBtn}${aclHtml}
${keys.filter(k=>!skip.includes(k)).map(k=>`${k}: ${typeof i[k]==='object'?JSON.stringify(i[k]):i[k]}`).join('
')}
`}).join('')}
`}
+function _fmtBytes(b){if(!b&&b!==0)return'—';if(b<1024)return b+' B';if(b<1048576)return(b/1024).toFixed(1)+' KB';if(b<1073741824)return(b/1048576).toFixed(1)+' MB';if(b<1099511627776)return(b/1073741824).toFixed(1)+' GB';return(b/1099511627776).toFixed(2)+' TB'}
+function rExpVcns(d){
+ return`${d.length} VCN(s)
${d.map((v,idx)=>{
+ const expanded=S._expExpanded&&S._expExpanded['vcn'+idx];
+ return`
+
${v.display_name} ${v.lifecycle_state}
+
${expanded?'▼':'▶'} ${(v.cidr_blocks||[]).length} CIDR(s)
+
id: ${v.id}
+${expanded?`
+
+| CIDR Blocks | ${(v.cidr_blocks||[]).join(', ')} |
+| DNS Label | ${v.dns_label||'—'} |
+| VCN Domain | ${v.vcn_domain_name||'—'} |
+| Default Route Table | ${v.default_route_table_id||'—'} |
+| Default Security List | ${v.default_security_list_id||'—'} |
+| Criado em | ${v.time_created||'—'} |
+
`:''}
`}).join('')}`}
+function rExpSubnets(d){
+ return`${d.length} subnet(s)
${d.map((s,idx)=>{
+ const expanded=S._expExpanded&&S._expExpanded['sub'+idx];
+ return`
+
${s.display_name} ${s.lifecycle_state} ${s.cidr_block}
+
${expanded?'▼':'▶'} ${s.availability_domain||'Regional'}
+
id: ${s.id}
+${expanded?`
+
+| CIDR | ${s.cidr_block} |
+| Availability Domain | ${s.availability_domain||'Regional'} |
+| DNS Label | ${s.dns_label||'—'} |
+| Subnet Domain | ${s.subnet_domain_name||'—'} |
+| IP Público | ${s.prohibit_public_ip_on_vnic?'Proibido (privada)':'Permitido (pública)'} |
+| Route Table | ${s.route_table_id||'—'} |
+| Security Lists | ${(s.security_list_ids||[]).join(' ')||'—'} |
+| VCN | ${s.vcn_id||'—'} |
+| Criado em | ${s.time_created||'—'} |
+
`:''}
`}).join('')}`}
+function rExpLoadBalancers(d){
+ return`${d.length} load balancer(s)
${d.map((lb,idx)=>{
+ const expanded=S._expExpanded&&S._expExpanded['lb'+idx];
+ const ips=(lb.ip_addresses||[]).join(', ');
+ return`
+
${lb.display_name} ${lb.lifecycle_state} ${lb.is_private?'Privado':'Público'}
+
${expanded?'▼':'▶'} ${lb.shape_name} · ${(lb.listeners||[]).length} listener(s)
+
IPs: ${ips||'—'}
+${expanded?`
+${(lb.listeners||[]).length?`
Listeners (${lb.listeners.length})
+
| Nome | Porta | Protocolo | Backend Set |
+${lb.listeners.map(l=>`| ${l.name} | ${l.port} | ${l.protocol} | ${l.backend_set||'—'} |
`).join('')}
+
`:''}
+${(lb.backend_sets||[]).length?`
Backend Sets (${lb.backend_sets.length})
+${lb.backend_sets.map(bs=>`
${bs.name} policy: ${bs.policy||'—'}
+${(bs.backends||[]).length?`
| IP | Porta | Peso |
+${bs.backends.map(b=>`| ${b.ip} | ${b.port} | ${b.weight} |
`).join('')}
+
`:'
Nenhum backend
'}
`).join('')}`:''}
+
`:''}
`}).join('')}`}
+function rExpBuckets(d){
+ return`${d.length} bucket(s)
${d.map(b=>{
+ const vis=b.public_access_type==='NoPublicAccess'?'Privado':'Público ('+b.public_access_type+')';
+ const visBadge=b.public_access_type==='NoPublicAccess'?'b-pass':'b-pend';
+ return`
${b.name} ${vis}
+
Objetos: ${b.approximate_count!=null?b.approximate_count.toLocaleString():'—'}
+Tamanho: ${_fmtBytes(b.approximate_size)}
+
Storage Tier: ${b.storage_tier||'—'}
+Versionamento: ${b.versioning||'—'}
+
Namespace: ${b.namespace}
+Criado: ${b.time_created||'—'}
`}).join('')}
`}
function rExpSecLists(d){
return`${d.length} security list(s)
${d.map((sl,idx)=>{
const rules=sl.rules||[];