feat: region safety guard, rollback button, SSH key support, split-panel layout
- Block terraform plan if model omits variable "region" (prevents wrong-region provisioning) - Rollback button on apply failure with ROLLBACK confirmation modal - SSH public key field in OCI configs, auto-injected into terraform.tfvars - Split-panel layout: terminal right (40%), files/plan/resources below chat (60%) - reasoning_effort uppercase fix for OCI SDK - Plan button persists after destroy status
This commit is contained in:
@@ -68,8 +68,10 @@ The platform combines security compliance scanning, AI-powered chat with **RAG (
|
|||||||
- Terraform CLI installed in container (v1.7.5)
|
- Terraform CLI installed in container (v1.7.5)
|
||||||
- **Terraform Resource Reference**: auto-generated OCI provider resource catalog (~937 resources) injected into system prompt — built from `terraform providers schema -json` at container startup, stored in SQLite, refreshable via menu button
|
- **Terraform Resource Reference**: auto-generated OCI provider resource catalog (~937 resources) injected into system prompt — built from `terraform providers schema -json` at container startup, stored in SQLite, refreshable via menu button
|
||||||
- **Official Resource Docs Injection**: on-demand fetch of Example Usage + Argument Reference from the official OCI Terraform provider docs (registry.terraform.io) — cached in SQLite, injected into model context based on detected resource types
|
- **Official Resource Docs Injection**: on-demand fetch of Example Usage + Argument Reference from the official OCI Terraform provider docs (registry.terraform.io) — cached in SQLite, injected into model context based on detected resource types
|
||||||
- **Smart variable resolution**: auto-generates `terraform.tfvars` by scanning declared variables and mapping them to OCI config values (`tenancy_ocid`, `user_ocid`, `fingerprint`, `compartment_id`) — region is never forced, respecting variable defaults
|
- **Smart variable resolution**: auto-generates `terraform.tfvars` by scanning declared variables and mapping them to OCI config values (`tenancy_ocid`, `user_ocid`, `fingerprint`, `compartment_id`, `ssh_public_key`) — region is never forced, respecting variable defaults
|
||||||
- **Multi-region provider management**: auto-detects provider aliases and region variables in generated code, generates `provider.tf` with credentials and correct region references (`var.region` instead of hardcoded values)
|
- **Multi-region provider management**: auto-detects provider aliases and region variables in generated code, generates `provider.tf` with credentials and correct region references (`var.region` instead of hardcoded values)
|
||||||
|
- **Region safety guard**: plan is blocked if the model does not declare `variable "region"` — prevents accidental provisioning in the wrong region by never falling back to hardcoded defaults
|
||||||
|
- **Rollback button**: when `terraform apply` fails, a Rollback button appears allowing manual `terraform destroy` to clean up partially created resources (requires typing "ROLLBACK" to confirm)
|
||||||
- **System prompt sync**: Terraform system prompt in code is automatically synced to DB on every restart — single source of truth
|
- **System prompt sync**: Terraform system prompt in code is automatically synced to DB on every restart — single source of truth
|
||||||
- Compact system prompt optimized for OCI Terraform provider best practices (100K max_tokens)
|
- Compact system prompt optimized for OCI Terraform provider best practices (100K max_tokens)
|
||||||
- **Flat workspace enforcement**: system prompt prohibits `module` blocks (workspace has no subdirectories) and enforces single-file variable declarations (`variables.tf` only)
|
- **Flat workspace enforcement**: system prompt prohibits `module` blocks (workspace has no subdirectories) and enforces single-file variable declarations (`variables.tf` only)
|
||||||
@@ -683,6 +685,7 @@ All models include OCID mapping for `us-ashburn-1`. For other regions, use the "
|
|||||||
|
|
||||||
| Version | Date | Changes |
|
| Version | Date | Changes |
|
||||||
|---------|------|---------|
|
|---------|------|---------|
|
||||||
|
| **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 |
|
||||||
| **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 |
|
| **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 |
|
||||||
| **v2.1** | 2026-03 | Terraform Agent (AI-powered IaC generation with plan/apply/destroy lifecycle, workspace management, Terraform CLI in container, inline file editor, split-panel terminal, multi-file generation via `// filename:` markers, **smart file correction** with merge-based updates, **14-point validation checklist**, 100K max_tokens), **3-layer auto-split pipeline** (frontend + backend monolithic HCL splitting into categorized files + backend deduplication), **HCL syntax auto-fix** (single-line blocks expanded to multi-line), **SQLite-based resource type validation** (~937 types with `difflib` suggestions for invalid types), **Terraform Resource Reference** (auto-generated OCI provider catalog from `terraform providers schema -json`, generated at startup to survive volume mounts, stored in SQLite, refreshable via menu button), compact system prompt for Terraform agent, ChatGPT-style chat history for both Chat Agent and Terraform Agent (rename/delete sessions), OCI Resource Actions (start/stop instances and Autonomous DBs from **OCI Account Explorer**), OCI Explorer expanded to 40+ resource types across 8 categories with tree-view navigation and resizable panels, compartment filtering for MCP CIS scans, chat sidebar with model parameters, chat audit logs, collection error tracking in MCP server, tool timeout increased to 30min with auto-retry, memory compaction tuned (6K threshold, 20 recent messages), model catalog trimmed from 69 to 15 curated models, improved GenAI response extraction with fallback for empty responses, fixed layout overflow (no page scroll), provider alias detection with brace-matching parser, multi-region Terraform support |
|
| **v2.1** | 2026-03 | Terraform Agent (AI-powered IaC generation with plan/apply/destroy lifecycle, workspace management, Terraform CLI in container, inline file editor, split-panel terminal, multi-file generation via `// filename:` markers, **smart file correction** with merge-based updates, **14-point validation checklist**, 100K max_tokens), **3-layer auto-split pipeline** (frontend + backend monolithic HCL splitting into categorized files + backend deduplication), **HCL syntax auto-fix** (single-line blocks expanded to multi-line), **SQLite-based resource type validation** (~937 types with `difflib` suggestions for invalid types), **Terraform Resource Reference** (auto-generated OCI provider catalog from `terraform providers schema -json`, generated at startup to survive volume mounts, stored in SQLite, refreshable via menu button), compact system prompt for Terraform agent, ChatGPT-style chat history for both Chat Agent and Terraform Agent (rename/delete sessions), OCI Resource Actions (start/stop instances and Autonomous DBs from **OCI Account Explorer**), OCI Explorer expanded to 40+ resource types across 8 categories with tree-view navigation and resizable panels, compartment filtering for MCP CIS scans, chat sidebar with model parameters, chat audit logs, collection error tracking in MCP server, tool timeout increased to 30min with auto-retry, memory compaction tuned (6K threshold, 20 recent messages), model catalog trimmed from 69 to 15 curated models, improved GenAI response extraction with fallback for empty responses, fixed layout overflow (no page scroll), provider alias detection with brace-matching parser, multi-region Terraform support |
|
||||||
|
|||||||
@@ -380,6 +380,11 @@ 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"]:
|
||||||
|
try:
|
||||||
|
c.execute(f"ALTER TABLE oci_configs ADD COLUMN {col}")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass
|
||||||
# Migrate legacy table_name from adb_vector_configs into adb_vector_tables
|
# Migrate legacy table_name from adb_vector_configs into adb_vector_tables
|
||||||
try:
|
try:
|
||||||
for cfg_row in c.execute("SELECT id, table_name FROM adb_vector_configs WHERE table_name IS NOT NULL AND table_name != ''").fetchall():
|
for cfg_row in c.execute("SELECT id, table_name FROM adb_vector_configs WHERE table_name IS NOT NULL AND table_name != ''").fetchall():
|
||||||
@@ -641,6 +646,7 @@ async def save_oci(
|
|||||||
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(""),
|
||||||
private_key: UploadFile = File(...), public_key: Optional[UploadFile] = File(None),
|
private_key: UploadFile = File(...), public_key: Optional[UploadFile] = File(None),
|
||||||
u = Depends(require("admin","user"))
|
u = Depends(require("admin","user"))
|
||||||
):
|
):
|
||||||
@@ -664,8 +670,8 @@ async def save_oci(
|
|||||||
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) 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) 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))
|
(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))
|
||||||
_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}")
|
||||||
_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})", u["id"], u["username"])
|
||||||
return {"id": cid, "tenancy_name": tenancy_name, "region": region}
|
return {"id": cid, "tenancy_name": tenancy_name, "region": region}
|
||||||
@@ -673,7 +679,7 @@ async def save_oci(
|
|||||||
@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,created_at"
|
cols = "id,user_id,tenancy_name,tenancy_ocid,user_ocid,fingerprint,region,compartment_id,ssh_public_key,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 = []
|
||||||
@@ -694,6 +700,10 @@ async def list_oci(u=Depends(current_user)):
|
|||||||
d["compartment_id"] = _mask(_dec(d["compartment_id"]))
|
d["compartment_id"] = _mask(_dec(d["compartment_id"]))
|
||||||
except Exception:
|
except Exception:
|
||||||
d["compartment_id"] = _mask(d["compartment_id"])
|
d["compartment_id"] = _mask(d["compartment_id"])
|
||||||
|
if d.get("ssh_public_key"):
|
||||||
|
# Show just type + first/last few chars for identification
|
||||||
|
parts = d["ssh_public_key"].split()
|
||||||
|
d["ssh_public_key_preview"] = f"{parts[0]} ...{parts[1][-12:]}" if len(parts) >= 2 else "ssh-rsa ..."
|
||||||
result.append(d)
|
result.append(d)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -715,6 +725,7 @@ async def update_oci(
|
|||||||
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(""),
|
||||||
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"))
|
||||||
):
|
):
|
||||||
@@ -727,6 +738,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_ssh = existing["ssh_public_key"] or ""
|
||||||
|
except (IndexError, KeyError):
|
||||||
|
existing_ssh = ""
|
||||||
|
ssh_public_key = ssh_public_key.strip() if ssh_public_key.strip() else existing_ssh
|
||||||
cdir = OCI_DIR / cid; cdir.mkdir(parents=True, exist_ok=True)
|
cdir = OCI_DIR / cid; cdir.mkdir(parents=True, exist_ok=True)
|
||||||
kp = cdir / "oci_api_key.pem"
|
kp = cdir / "oci_api_key.pem"
|
||||||
if private_key and private_key.filename:
|
if private_key and private_key.filename:
|
||||||
@@ -744,8 +760,8 @@ async def update_oci(
|
|||||||
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=? WHERE id=?",
|
c.execute("UPDATE oci_configs SET tenancy_name=?,tenancy_ocid=?,user_ocid=?,fingerprint=?,region=?,compartment_id=?,ssh_public_key=? WHERE id=?",
|
||||||
(tenancy_name, _enc(tenancy_ocid), _enc(user_ocid), _enc(fingerprint), region, _enc(compartment_id) if compartment_id else 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, 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}")
|
||||||
_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})", u["id"], u["username"])
|
||||||
return {"id": cid, "tenancy_name": tenancy_name, "region": region}
|
return {"id": cid, "tenancy_name": tenancy_name, "region": region}
|
||||||
@@ -1903,7 +1919,7 @@ def _call_genai(gc: dict, message: str, history: list = None, tools: list = None
|
|||||||
chat_request.max_completion_tokens = clamped_tokens
|
chat_request.max_completion_tokens = clamped_tokens
|
||||||
re = gc.get("reasoning_effort")
|
re = gc.get("reasoning_effort")
|
||||||
if re and hasattr(chat_request, "reasoning_effort"):
|
if re and hasattr(chat_request, "reasoning_effort"):
|
||||||
chat_request.reasoning_effort = re
|
chat_request.reasoning_effort = re.upper() if isinstance(re, str) else re
|
||||||
# Explicitly unset unsupported params
|
# Explicitly unset unsupported params
|
||||||
chat_request.temperature = None
|
chat_request.temperature = None
|
||||||
chat_request.top_p = None
|
chat_request.top_p = None
|
||||||
@@ -1940,6 +1956,16 @@ def _call_genai(gc: dict, message: str, history: list = None, tools: list = None
|
|||||||
chat_request.frequency_penalty = None
|
chat_request.frequency_penalty = None
|
||||||
chat_request.presence_penalty = None
|
chat_request.presence_penalty = None
|
||||||
|
|
||||||
|
# Log applied parameters
|
||||||
|
params_log = f"provider={provider}, reasoning={is_reasoning}"
|
||||||
|
params_log += f", max_tokens={getattr(chat_request, 'max_completion_tokens', None) or getattr(chat_request, 'max_tokens', None)}"
|
||||||
|
params_log += f", temp={getattr(chat_request, 'temperature', None)}, top_p={getattr(chat_request, 'top_p', None)}"
|
||||||
|
params_log += f", top_k={getattr(chat_request, 'top_k', None)}"
|
||||||
|
params_log += f", freq_pen={getattr(chat_request, 'frequency_penalty', None)}, pres_pen={getattr(chat_request, 'presence_penalty', None)}"
|
||||||
|
if is_reasoning:
|
||||||
|
params_log += f", reasoning_effort={getattr(chat_request, 'reasoning_effort', None)}"
|
||||||
|
log.info(f"GenAI params: {params_log}")
|
||||||
|
|
||||||
messages = []
|
messages = []
|
||||||
if system_prompt:
|
if system_prompt:
|
||||||
sys_content = oci.generative_ai_inference.models.TextContent()
|
sys_content = oci.generative_ai_inference.models.TextContent()
|
||||||
@@ -2181,7 +2207,7 @@ Se pedirem AWS, Azure, GCP ou outro provider, recuse educadamente.
|
|||||||
- Nunca declare o mesmo recurso (tipo + nome) em mais de um arquivo.
|
- Nunca declare o mesmo recurso (tipo + nome) em mais de um arquivo.
|
||||||
- Use `var.compartment_id` nos recursos e declare sempre `variable "region"` em `variables.tf`.
|
- Use `var.compartment_id` nos recursos e declare sempre `variable "region"` em `variables.tf`.
|
||||||
- Se houver "RECURSOS OCI EXISTENTES NO COMPARTMENT", reutilize recursos com data sources sempre que possível.
|
- Se houver "RECURSOS OCI EXISTENTES NO COMPARTMENT", reutilize recursos com data sources sempre que possível.
|
||||||
- Se houver "ARQUIVOS TERRAFORM ATUAIS NO WORKSPACE", gere somente os arquivos corrigidos, com os mesmos nomes.
|
- Se houver "ARQUIVOS TERRAFORM ATUAIS NO WORKSPACE", gere **SOMENTE os arquivos que precisam de correção**. Arquivos sem erro NÃO devem ser regenerados. Mantenha os mesmos nomes de arquivo. Se o erro está em `networking.tf`, gere APENAS `// filename: networking.tf` com o conteúdo corrigido. NUNCA regenere `variables.tf`, `outputs.tf` ou outros arquivos que não têm erro, a menos que o erro exija alteração neles (ex: variável faltando).
|
||||||
- Sempre responda com blocos `hcl`, depois **Resource Plan** (`+` criar, `~` reutilizar), uma breve explicação e uma seção `✅ Validação`.
|
- Sempre responda com blocos `hcl`, depois **Resource Plan** (`+` criar, `~` reutilizar), uma breve explicação e uma seção `✅ Validação`.
|
||||||
- Valide: referências cruzadas, CIDRs, route tables, security lists, dependências, segurança e variáveis.
|
- Valide: referências cruzadas, CIDRs, route tables, security lists, dependências, segurança e variáveis.
|
||||||
- Use apenas resource types válidos do OCI. Exemplos corretos:
|
- Use apenas resource types válidos do OCI. Exemplos corretos:
|
||||||
@@ -2197,6 +2223,13 @@ Se pedirem AWS, Azure, GCP ou outro provider, recuse educadamente.
|
|||||||
- Nunca invente recursos inexistentes.
|
- Nunca invente recursos inexistentes.
|
||||||
- Use sempre HCL multi-line, nunca blocos inline com múltiplos argumentos.
|
- Use sempre HCL multi-line, nunca blocos inline com múltiplos argumentos.
|
||||||
|
|
||||||
|
### Correção de erros (IMPORTANTE)
|
||||||
|
- Quando o usuário envia um erro de plan/apply com arquivos existentes, gere SOMENTE o(s) arquivo(s) que corrigem o erro.
|
||||||
|
- Exemplo: se o erro é em `service_gateways.tf`, retorne APENAS `// filename: service_gateways.tf` com o conteúdo corrigido.
|
||||||
|
- Se a correção exige criar/alterar uma variável, inclua também `// filename: variables.tf` apenas com as variáveis novas/alteradas + todas existentes.
|
||||||
|
- NUNCA regenere todos os arquivos do zero. Isso descarta o trabalho anterior e cria loops infinitos de correção.
|
||||||
|
- Preserve os nomes de arquivo exatos. Não renomeie `drg_attachments_and_routes.tf` para `drg.tf`.
|
||||||
|
|
||||||
### Regras críticas
|
### Regras críticas
|
||||||
- `oci_core_drg_route_distribution_statement` deve usar `match_criteria`.
|
- `oci_core_drg_route_distribution_statement` deve usar `match_criteria`.
|
||||||
- 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).
|
||||||
@@ -4021,11 +4054,13 @@ async def _chat_background(mid: str, sid: str, msg: ChatMsg, user: dict, genai_c
|
|||||||
base_prompt = cfg_dict.get("system_prompt", "")
|
base_prompt = cfg_dict.get("system_prompt", "")
|
||||||
cfg_dict["system_prompt"] = f"{base_prompt}\n\n{config_hint}" if base_prompt else config_hint
|
cfg_dict["system_prompt"] = f"{base_prompt}\n\n{config_hint}" if base_prompt else config_hint
|
||||||
|
|
||||||
# ── Terraform agent: boost max_tokens to model limit ──
|
# ── Terraform agent: boost max_tokens to model limit + force reasoning_effort=high ──
|
||||||
if agent_type == "terraform":
|
if agent_type == "terraform":
|
||||||
tf_model_info = GENAI_MODELS.get(cfg_dict.get("model_id", ""), {})
|
tf_model_info = GENAI_MODELS.get(cfg_dict.get("model_id", ""), {})
|
||||||
tf_model_max = tf_model_info.get("max_tokens", 32768)
|
tf_model_max = tf_model_info.get("max_tokens", 32768)
|
||||||
cfg_dict["max_tokens"] = tf_model_max
|
cfg_dict["max_tokens"] = tf_model_max
|
||||||
|
if tf_model_info.get("reasoning") and not cfg_dict.get("reasoning_effort"):
|
||||||
|
cfg_dict["reasoning_effort"] = "HIGH"
|
||||||
|
|
||||||
# ── Inject existing OCI resources for terraform agent ──
|
# ── Inject existing OCI resources for terraform agent ──
|
||||||
if agent_type == "terraform" and active_oci_id:
|
if agent_type == "terraform" and active_oci_id:
|
||||||
@@ -5129,11 +5164,22 @@ async def _terraform_exec(wid: str, action: str, user: dict):
|
|||||||
if passphrase:
|
if passphrase:
|
||||||
cred_block += f'\n private_key_password = "{passphrase}"'
|
cred_block += f'\n private_key_password = "{passphrase}"'
|
||||||
|
|
||||||
# Primary region: use var.region if declared in .tf files, otherwise hardcode from OCI config
|
# Primary region: MUST use var.region — model is required to declare it
|
||||||
with db() as c:
|
with db() as c:
|
||||||
oci_row = c.execute("SELECT region FROM oci_configs WHERE id=?", (ws["oci_config_id"],)).fetchone()
|
oci_row = c.execute("SELECT region FROM oci_configs WHERE id=?", (ws["oci_config_id"],)).fetchone()
|
||||||
primary_region = oci_row["region"] if oci_row else oci_cfg.get("region", "")
|
primary_region = oci_row["region"] if oci_row else oci_cfg.get("region", "")
|
||||||
region_value = 'var.region' if has_region_var else f'"{primary_region}"'
|
if not has_region_var:
|
||||||
|
error_msg = (
|
||||||
|
f'ERRO: O código Terraform não declarou variable "region". '
|
||||||
|
f'Isso é obrigatório para garantir que os recursos sejam provisionados na região correta. '
|
||||||
|
f'Adicione no variables.tf: variable "region" {{ type = string; default = "{primary_region}" }}'
|
||||||
|
)
|
||||||
|
log.warning(f"Workspace {wid}: missing variable 'region' — blocking plan")
|
||||||
|
with db() as c:
|
||||||
|
c.execute("UPDATE terraform_workspaces SET status='failed', error=?, updated_at=datetime('now') WHERE id=?",
|
||||||
|
(error_msg, wid))
|
||||||
|
return
|
||||||
|
region_value = 'var.region'
|
||||||
|
|
||||||
provider_tf = f'''terraform {{
|
provider_tf = f'''terraform {{
|
||||||
required_providers {{
|
required_providers {{
|
||||||
@@ -5165,8 +5211,13 @@ provider "oci" {{
|
|||||||
|
|
||||||
# Auto-generate terraform.tfvars — scan declared variables and provide values from OCI config
|
# Auto-generate terraform.tfvars — scan declared variables and provide values from OCI config
|
||||||
with db() as c:
|
with db() as c:
|
||||||
oci_row = c.execute("SELECT compartment_id, region FROM oci_configs WHERE id=?", (ws["oci_config_id"],)).fetchone()
|
oci_row = c.execute("SELECT compartment_id, region, ssh_public_key FROM oci_configs WHERE id=?", (ws["oci_config_id"],)).fetchone()
|
||||||
comp_id = ws["compartment_id"] if ws["compartment_id"] else (_safe_dec(oci_row["compartment_id"]) if oci_row and oci_row["compartment_id"] else "")
|
comp_id = ws["compartment_id"] if ws["compartment_id"] else (_safe_dec(oci_row["compartment_id"]) if oci_row and oci_row["compartment_id"] else "")
|
||||||
|
try:
|
||||||
|
ssh_pub_key = oci_row["ssh_public_key"] if oci_row else ""
|
||||||
|
except (IndexError, KeyError):
|
||||||
|
ssh_pub_key = ""
|
||||||
|
ssh_pub_key = ssh_pub_key or ""
|
||||||
|
|
||||||
# Scan all declared variables in .tf files
|
# Scan all declared variables in .tf files
|
||||||
declared_vars = set()
|
declared_vars = set()
|
||||||
@@ -5185,6 +5236,8 @@ provider "oci" {{
|
|||||||
"user_ocid": oci_cfg.get("user", ""),
|
"user_ocid": oci_cfg.get("user", ""),
|
||||||
"fingerprint": oci_cfg.get("fingerprint", ""),
|
"fingerprint": oci_cfg.get("fingerprint", ""),
|
||||||
"private_key_path": oci_cfg.get("key_file", ""),
|
"private_key_path": oci_cfg.get("key_file", ""),
|
||||||
|
"ssh_public_key": ssh_pub_key,
|
||||||
|
"ssh_authorized_keys": ssh_pub_key,
|
||||||
}
|
}
|
||||||
# NOTE: "region" is intentionally NOT included — Terraform uses the default from variable declarations
|
# NOTE: "region" is intentionally NOT included — Terraform uses the default from variable declarations
|
||||||
for vname, vval in oci_var_map.items():
|
for vname, vval in oci_var_map.items():
|
||||||
|
|||||||
@@ -375,11 +375,15 @@ html .spinner,html [class*="kpi-bar-fill"],html canvas{transition:none!important
|
|||||||
.exp-cat{padding:.3rem .5rem;font-size:.6rem}
|
.exp-cat{padding:.3rem .5rem;font-size:.6rem}
|
||||||
}
|
}
|
||||||
/* ── Terraform Agent ── */
|
/* ── Terraform Agent ── */
|
||||||
.tf-wrap{display:flex;flex-direction:column;height:calc(100vh - 56px - 3rem);overflow:hidden}
|
.tf-wrap{display:flex;flex-direction:row;height:calc(100vh - 56px - 3rem);overflow:hidden}
|
||||||
.tf-chat{flex:1;display:flex;flex-direction:column;overflow:hidden;min-height:0}
|
.tf-left{flex:6;display:flex;flex-direction:column;overflow:hidden;min-width:0}
|
||||||
|
.tf-chat{flex:7;display:flex;flex-direction:column;overflow:hidden;min-height:0}
|
||||||
|
.tf-output-panel{flex:4;display:flex;flex-direction:column;overflow:hidden;border-left:1px solid var(--bd);min-width:0}
|
||||||
.tf-toolbar{display:flex;align-items:center;gap:.5rem;padding:.45rem .7rem;border-bottom:1px solid var(--bd);flex-wrap:wrap;background:var(--bg)}
|
.tf-toolbar{display:flex;align-items:center;gap:.5rem;padding:.45rem .7rem;border-bottom:1px solid var(--bd);flex-wrap:wrap;background:var(--bg)}
|
||||||
.tf-msgs{flex:1;overflow-y:auto;padding:.7rem}
|
.tf-msgs{flex:1;overflow-y:auto;padding:.7rem}
|
||||||
.tfp-act{display:inline-flex;align-items:center;gap:.25rem;padding:.2rem .5rem;font-size:.6rem;border-radius:5px;border:1px solid var(--bd);background:var(--bg2);color:var(--t2);cursor:pointer;transition:all .15s;font-family:inherit;line-height:1.3}
|
.tfp-acts{display:flex;gap:.35rem;margin-top:.5rem;padding-top:.45rem;border-top:1px solid var(--bd)}
|
||||||
|
.tfp-act{display:inline-flex;align-items:center;gap:.2rem;padding:.2rem .45rem;font-size:.62rem;border-radius:5px;border:1px solid var(--bd);background:var(--bg2);color:var(--t2);cursor:pointer;transition:all .15s;font-family:inherit;line-height:1.3}
|
||||||
|
.tfp-act svg{width:11px;height:11px}
|
||||||
.tfp-act:hover{background:var(--bg3);border-color:var(--t4)}
|
.tfp-act:hover{background:var(--bg3);border-color:var(--t4)}
|
||||||
.tfp-act-tf{background:#7b42bc18;border-color:#7b42bc55;color:#7b42bc}
|
.tfp-act-tf{background:#7b42bc18;border-color:#7b42bc55;color:#7b42bc}
|
||||||
.tfp-act-tf:hover{background:#7b42bc30;border-color:#7b42bc}
|
.tfp-act-tf:hover{background:#7b42bc30;border-color:#7b42bc}
|
||||||
@@ -389,19 +393,17 @@ html .spinner,html [class*="kpi-bar-fill"],html canvas{transition:none!important
|
|||||||
.tf-resize:hover,.tf-resize.dragging{background:var(--pp);opacity:.7}
|
.tf-resize:hover,.tf-resize.dragging{background:var(--pp);opacity:.7}
|
||||||
.tf-resize::after{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:28px;height:3px;border-top:1px solid var(--t4);border-bottom:1px solid var(--t4);opacity:.4}
|
.tf-resize::after{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:28px;height:3px;border-top:1px solid var(--t4);border-bottom:1px solid var(--t4);opacity:.4}
|
||||||
.tf-resize:hover::after{border-color:var(--pp);opacity:.8}
|
.tf-resize:hover::after{border-color:var(--pp);opacity:.8}
|
||||||
.tf-bottom{flex:1;display:flex;flex-direction:column;overflow:hidden;border-top:1px solid var(--bd);background:var(--bg1);min-height:0}
|
.tf-bottom{flex:3;display:flex;flex-direction:column;overflow:hidden;border-top:1px solid var(--bd);background:var(--bg1);min-height:0}
|
||||||
.tf-bottom-tabs{display:flex;align-items:center;gap:0;border-bottom:1px solid var(--bd);background:var(--bg);padding:0 .5rem}
|
.tf-bottom-tabs{display:flex;align-items:center;gap:0;border-bottom:1px solid var(--bd);background:var(--bg);padding:0 .5rem}
|
||||||
.tf-btab{padding:.4rem .75rem;font-size:.72rem;font-weight:600;cursor:pointer;border-bottom:2px solid transparent;color:var(--t3);display:flex;align-items:center;gap:.3rem;transition:all .15s}
|
.tf-btab{padding:.4rem .75rem;font-size:.72rem;font-weight:600;cursor:pointer;border-bottom:2px solid transparent;color:var(--t3);display:flex;align-items:center;gap:.3rem;transition:all .15s}
|
||||||
.tf-btab:hover{color:var(--t1)}
|
.tf-btab:hover{color:var(--t1)}
|
||||||
.tf-btab.active{color:var(--pp);border-bottom-color:var(--pp)}
|
.tf-btab.active{color:var(--pp);border-bottom-color:var(--pp)}
|
||||||
.tf-btab svg{width:14px;height:14px}
|
.tf-btab svg{width:14px;height:14px}
|
||||||
.tf-bottom-actions{margin-left:auto;display:flex;align-items:center;gap:.4rem;padding-right:.3rem}
|
.tf-bottom-actions{margin-left:auto;display:flex;align-items:center;gap:.4rem;padding-right:.3rem}
|
||||||
.tf-bottom-content{flex:1;overflow:hidden;display:flex;flex-direction:row}
|
.tf-bottom-content{flex:1;overflow:auto;padding:.5rem .7rem}
|
||||||
.tf-bc-left{flex:1;overflow:auto;padding:.5rem .7rem;min-width:0}
|
.tf-output-panel .tf-term-hdr{padding:.3rem .6rem;font-size:.68rem;font-weight:700;color:var(--t2);background:var(--bg);border-bottom:1px solid var(--bd);display:flex;align-items:center;gap:.4rem}
|
||||||
.tf-bc-right{width:50%;overflow:auto;border-left:1px solid var(--bd);flex-shrink:0;display:flex;flex-direction:column}
|
.tf-output-panel .tf-term-hdr svg{width:12px;height:12px;fill:var(--t2)}
|
||||||
.tf-bc-right .tf-term-hdr{padding:.3rem .6rem;font-size:.68rem;font-weight:700;color:var(--t2);background:var(--bg);border-bottom:1px solid var(--bd);display:flex;align-items:center;gap:.4rem}
|
.tf-output-panel .tf-term-body{flex:1;overflow:auto;background:var(--bg);padding:.5rem;font-family:var(--fm);font-size:.68rem;line-height:1.5;color:var(--t2);white-space:pre-wrap;word-break:break-word}
|
||||||
.tf-bc-right .tf-term-hdr svg{width:12px;height:12px;fill:var(--t2)}
|
|
||||||
.tf-bc-right .tf-term-body{flex:1;overflow:auto;background:var(--bg);padding:.5rem;font-family:var(--fm);font-size:.68rem;line-height:1.5;color:var(--t2);white-space:pre-wrap;word-break:break-word}
|
|
||||||
.tf-file{display:flex;align-items:center;gap:.5rem;padding:.45rem .6rem;border-radius:8px;border:1px solid var(--bd);margin-bottom:.4rem;background:var(--bg);transition:border-color .15s;cursor:pointer}
|
.tf-file{display:flex;align-items:center;gap:.5rem;padding:.45rem .6rem;border-radius:8px;border:1px solid var(--bd);margin-bottom:.4rem;background:var(--bg);transition:border-color .15s;cursor:pointer}
|
||||||
.tf-file:hover{border-color:var(--pp)}
|
.tf-file:hover{border-color:var(--pp)}
|
||||||
.tf-file.editing{border-color:var(--pp);background:rgba(191,90,242,.06)}
|
.tf-file.editing{border-color:var(--pp);background:rgba(191,90,242,.06)}
|
||||||
@@ -455,6 +457,9 @@ html .spinner,html [class*="kpi-bar-fill"],html canvas{transition:none!important
|
|||||||
.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){
|
||||||
|
.tf-wrap{flex-direction:column}
|
||||||
|
.tf-left{flex:none;height:60%}
|
||||||
|
.tf-output-panel{flex:none;height:40%;border-left:none;border-top:1px solid var(--bd)}
|
||||||
.tf-bottom{min-height:0}
|
.tf-bottom{min-height:0}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -507,6 +512,7 @@ const IC={
|
|||||||
python:_i('<path d="M12 2C6.5 2 6 4.5 6 4.5V7h6v1H4.5S2 7.5 2 12s2 5 2 5h2v-3s0-2 2-2h6s2 0 2-2V5s0-3-4-3zm-2 2a1 1 0 110 2 1 1 0 010-2z"/><path d="M12 22c5.5 0 6-2.5 6-2.5V17h-6v-1h7.5s2.5.5 2.5-4-2-5-2-5h-2v3s0 2-2 2h-6s-2 0-2 2v5s0 3 4 3zm2-2a1 1 0 110-2 1 1 0 010 2z"/>'),
|
python:_i('<path d="M12 2C6.5 2 6 4.5 6 4.5V7h6v1H4.5S2 7.5 2 12s2 5 2 5h2v-3s0-2 2-2h6s2 0 2-2V5s0-3-4-3zm-2 2a1 1 0 110 2 1 1 0 010-2z"/><path d="M12 22c5.5 0 6-2.5 6-2.5V17h-6v-1h7.5s2.5.5 2.5-4-2-5-2-5h-2v3s0 2-2 2h-6s-2 0-2 2v5s0 3 4 3zm2-2a1 1 0 110-2 1 1 0 010 2z"/>'),
|
||||||
terminal:_i('<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>'),
|
terminal:_i('<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>'),
|
||||||
rocket:_i('<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 00-2.91-.09z"/><path d="M12 15l-3-3a22 22 0 012-3.95A12.88 12.88 0 0122 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 01-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/>'),
|
rocket:_i('<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 00-2.91-.09z"/><path d="M12 15l-3-3a22 22 0 012-3.95A12.88 12.88 0 0122 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 01-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/>'),
|
||||||
|
fix:_i('<path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z"/>'),
|
||||||
refresh:_i('<polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/>'),
|
refresh:_i('<polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/>'),
|
||||||
shield:_i('<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>'),
|
shield:_i('<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>'),
|
||||||
eye:_i('<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>'),
|
eye:_i('<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>'),
|
||||||
@@ -533,7 +539,7 @@ const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],g
|
|||||||
rptLevel:2,rptObp:false,rptRaw:false,rptRedact:false,reportFiles:{},dlExpandedRid:null,dlTenancyFilter:'',dlSectionFilter:'',
|
rptLevel:2,rptObp:false,rptRaw:false,rptRedact:false,reportFiles:{},dlExpandedRid:null,dlTenancyFilter:'',dlSectionFilter:'',
|
||||||
cisVer:null,cisCheckResult:null,cisUpdating:false,rptHistFilter:'',rptKpi:null,rptKpiLoading:false,chatFiles:[],
|
cisVer:null,cisCheckResult:null,cisUpdating:false,rptHistFilter:'',rptKpi:null,rptKpiLoading:false,chatFiles:[],
|
||||||
tfMsgs:[],tfSid:null,tfModel:'',tfOci:'',tfRegion:'',tfCompartment:'',tfPlan:[],tfCode:'',tfPanel:'',
|
tfMsgs:[],tfSid:null,tfModel:'',tfOci:'',tfRegion:'',tfCompartment:'',tfPlan:[],tfCode:'',tfPanel:'',
|
||||||
tfWs:null,tfWsList:[],tfPlanOut:'',tfApplyOut:'',tfDestroyOut:'',tfStatus:'draft',tfRunning:false,tfConfirmDest: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};
|
||||||
@@ -571,7 +577,7 @@ function R(){try{
|
|||||||
const scrollIds=['chm','tfchm','tfBottom','tfTermOut','pg'];
|
const scrollIds=['chm','tfchm','tfBottom','tfTermOut','pg'];
|
||||||
const scrollMap={};
|
const scrollMap={};
|
||||||
scrollIds.forEach(id=>{const el=document.getElementById(id);if(el)scrollMap[id]=el.scrollTop});
|
scrollIds.forEach(id=>{const el=document.getElementById(id);if(el)scrollMap[id]=el.scrollTop});
|
||||||
const bcLeft=document.querySelector('.tf-bc-left');if(bcLeft)scrollMap._bcLeft=bcLeft.scrollTop;
|
const bcLeft=document.querySelector('.tf-bottom-content');if(bcLeft)scrollMap._bcContent=bcLeft.scrollTop;
|
||||||
|
|
||||||
const appEl=document.getElementById('app');
|
const appEl=document.getElementById('app');
|
||||||
if(!S.user){appEl.innerHTML=rLogin();return}
|
if(!S.user){appEl.innerHTML=rLogin();return}
|
||||||
@@ -589,7 +595,7 @@ function R(){try{
|
|||||||
const ed2=document.getElementById('tfEdTa');if(ed2&&S._tfEdVal)ed2.value=S._tfEdVal;
|
const ed2=document.getElementById('tfEdTa');if(ed2&&S._tfEdVal)ed2.value=S._tfEdVal;
|
||||||
// Restore scroll positions
|
// Restore scroll positions
|
||||||
requestAnimationFrame(()=>{
|
requestAnimationFrame(()=>{
|
||||||
Object.entries(scrollMap).forEach(([id,top])=>{if(id==='_bcLeft'){const el=document.querySelector('.tf-bc-left');if(el)el.scrollTop=top}else{const el=document.getElementById(id);if(el)el.scrollTop=top}});
|
Object.entries(scrollMap).forEach(([id,top])=>{if(id==='_bcContent'){const el=document.querySelector('.tf-bottom-content');if(el)el.scrollTop=top}else{const el=document.getElementById(id);if(el)el.scrollTop=top}});
|
||||||
if(S.tfBottomH){const bel=document.getElementById('tfBottom');if(bel){bel.style.height=S.tfBottomH+'px';bel.style.flex='none'}}
|
if(S.tfBottomH){const bel=document.getElementById('tfBottom');if(bel){bel.style.height=S.tfBottomH+'px';bel.style.flex='none'}}
|
||||||
if(S.expTreeW){const tel=document.getElementById('exp-tree');if(tel)tel.style.width=S.expTreeW+'px'}
|
if(S.expTreeW){const tel=document.getElementById('exp-tree');if(tel)tel.style.width=S.expTreeW+'px'}
|
||||||
});
|
});
|
||||||
@@ -809,7 +815,7 @@ async function pollChatResult(mid){
|
|||||||
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 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.tfConfirmDest=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(){
|
||||||
const m=document.getElementById('tfMenu');
|
const m=document.getElementById('tfMenu');
|
||||||
@@ -884,6 +890,7 @@ async function loadSession(sid,type){
|
|||||||
S.tfWs=ws.id;S.tfStatus=ws.status;
|
S.tfWs=ws.id;S.tfStatus=ws.status;
|
||||||
const detail=await $api('/terraform/workspaces/'+ws.id);
|
const detail=await $api('/terraform/workspaces/'+ws.id);
|
||||||
S.tfPlanOut=detail.plan_output||'';S.tfApplyOut=detail.apply_output||'';S.tfDestroyOut=detail.destroy_output||'';
|
S.tfPlanOut=detail.plan_output||'';S.tfApplyOut=detail.apply_output||'';S.tfDestroyOut=detail.destroy_output||'';
|
||||||
|
if(S.tfApplyOut)S.tfHadApply=true;
|
||||||
if(detail.tf_code){
|
if(detail.tf_code){
|
||||||
S.tfCode=detail.tf_code;
|
S.tfCode=detail.tf_code;
|
||||||
const parts=detail.tf_code.split(/^\/\/\s*filename:\s*/m).filter(Boolean);
|
const parts=detail.tf_code.split(/^\/\/\s*filename:\s*/m).filter(Boolean);
|
||||||
@@ -1130,9 +1137,11 @@ function fmTf(t){
|
|||||||
S.tfPlan.map(x=>'<div class="tf-plan-item"><span class="tf-plan-add">+</span><span class="tf-plan-type">'+escHtml(x.type)+'</span>'+(x.desc?'<span class="tf-plan-desc">'+escHtml(x.desc)+'</span>':'')+'</div>').join('')+'</div></div>';
|
S.tfPlan.map(x=>'<div class="tf-plan-item"><span class="tf-plan-add">+</span><span class="tf-plan-type">'+escHtml(x.type)+'</span>'+(x.desc?'<span class="tf-plan-desc">'+escHtml(x.desc)+'</span>':'')+'</div>').join('')+'</div></div>';
|
||||||
}
|
}
|
||||||
// Auto-split: if model returned 1-2 large blocks, split into multiple files
|
// Auto-split: if model returned 1-2 large blocks, split into multiple files
|
||||||
// Runs on BOTH first generation and corrections — monolithic responses always get split
|
// Only auto-split on FIRST generation (no existing files) — during corrections, trust the model's named files
|
||||||
|
const hasExistingFiles=S.tfFiles.length>0;
|
||||||
|
const allNamed=codeBlocks.every(b=>b.name!=='main.tf'||codeBlocks.length===1);
|
||||||
let didAutoSplit=false;
|
let didAutoSplit=false;
|
||||||
if(codeBlocks.length<=2){
|
if(codeBlocks.length<=2 && !hasExistingFiles){
|
||||||
const totalContent=codeBlocks.map(b=>b.content).join('\n\n');
|
const totalContent=codeBlocks.map(b=>b.content).join('\n\n');
|
||||||
if(totalContent.length>2000){
|
if(totalContent.length>2000){
|
||||||
const split=_splitTfMonolith(totalContent);
|
const split=_splitTfMonolith(totalContent);
|
||||||
@@ -1196,9 +1205,9 @@ function rTfPrompt(){
|
|||||||
const msHtml=msgs.length===0
|
const msHtml=msgs.length===0
|
||||||
?`<div class="tf-empty">${IC.edit}<p>Descreva a infraestrutura OCI que você precisa</p><p style="font-size:.68rem;opacity:.6">O agente vai gerar um prompt estruturado e otimizado para o Terraform Agent.</p>
|
?`<div class="tf-empty">${IC.edit}<p>Descreva a infraestrutura OCI que você precisa</p><p style="font-size:.68rem;opacity:.6">O agente vai gerar um prompt estruturado e otimizado para o Terraform Agent.</p>
|
||||||
<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)}</div>${m._raw?`<div style="display:flex;gap:.3rem;margin-top:.35rem;padding-top:.3rem;border-top:1px solid var(--bd)">
|
:msgs.map((m,i)=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}${m._raw?`
|
||||||
<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>`).join('');
|
<button class="tfp-act tfp-act-tf" onclick="tfpSendToTf(${i})">${TF_IC} Terraform</button></div>`:''}</div></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})}});
|
||||||
@@ -1266,7 +1275,15 @@ async function _tfpResolveGenai(){
|
|||||||
return{oci_config_id:oc.id,model_id:'openai.gpt-4.1',genai_region:oc.region,compartment_id:oc.compartment_id}}
|
return{oci_config_id:oc.id,model_id:'openai.gpt-4.1',genai_region:oc.region,compartment_id:oc.compartment_id}}
|
||||||
function tfpCopy(idx){
|
function tfpCopy(idx){
|
||||||
const m=S.tfpMsgs[idx];if(!m||!m._raw)return;
|
const m=S.tfpMsgs[idx];if(!m||!m._raw)return;
|
||||||
navigator.clipboard.writeText(m._raw).then(()=>{m._copied=true;R();setTimeout(()=>{m._copied=false;R()},2000)})}
|
navigator.clipboard.writeText(m._raw).then(()=>{
|
||||||
|
// Show toast instead of re-rendering
|
||||||
|
const toast=document.createElement('div');
|
||||||
|
toast.textContent='Prompt copiado!';
|
||||||
|
toast.style.cssText='position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#2ecc71;color:#fff;padding:6px 16px;border-radius:6px;font-size:.75rem;z-index:9999;opacity:0;transition:opacity .2s';
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
requestAnimationFrame(()=>toast.style.opacity='1');
|
||||||
|
setTimeout(()=>{toast.style.opacity='0';setTimeout(()=>toast.remove(),200)},1500);
|
||||||
|
})}
|
||||||
function tfpSendToTf(idx){
|
function tfpSendToTf(idx){
|
||||||
const m=S.tfpMsgs[idx];if(!m||!m._raw)return;
|
const m=S.tfpMsgs[idx];if(!m||!m._raw)return;
|
||||||
S.tab='terraform';
|
S.tab='terraform';
|
||||||
@@ -1296,7 +1313,8 @@ function rTerraform(){
|
|||||||
const stBadge=_tfBadge(S.tfStatus);
|
const stBadge=_tfBadge(S.tfStatus);
|
||||||
|
|
||||||
return `<div class="tf-wrap">
|
return `<div class="tf-wrap">
|
||||||
<div style="display:flex;flex:1;min-height:0;overflow:hidden">
|
<div class="tf-left">
|
||||||
|
<div style="display:flex;flex:7;min-height:0;overflow:hidden">
|
||||||
<div class="ch-hp${S.tfHistOpen?' open':''}"><div class="ch-hp-inner">
|
<div class="ch-hp${S.tfHistOpen?' open':''}"><div class="ch-hp-inner">
|
||||||
<div class="ch-hp-hdr"><span>${IC.history} Histórico</span><div class="ch-hp-new" onclick="newTfChat()">+ Novo Chat</div></div>
|
<div class="ch-hp-hdr"><span>${IC.history} Histórico</span><div class="ch-hp-new" onclick="newTfChat()">+ Novo Chat</div></div>
|
||||||
<div class="ch-hp-list">${rHistList('terraform',S.tfHistory,S.tfSid)}</div>
|
<div class="ch-hp-list">${rHistList('terraform',S.tfHistory,S.tfSid)}</div>
|
||||||
@@ -1353,12 +1371,14 @@ function rTerraform(){
|
|||||||
${rTfActions()}
|
${rTfActions()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tf-bottom-content">
|
<div class="tf-bottom-content">${rTfBottomContent()}</div>
|
||||||
<div class="tf-bc-left">${rTfBottomContent()}</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tf-output-panel">
|
||||||
${rTfTerminal()}
|
${rTfTerminal()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
${S.tfConfirmDest?rTfConfirmDestroy():''}
|
${S.tfConfirmDest?rTfConfirmDestroy():''}
|
||||||
|
${S.tfConfirmRollback?rTfConfirmRollback():''}
|
||||||
${S.tfPromptOpen?rTfPromptModal():''}
|
${S.tfPromptOpen?rTfPromptModal():''}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -1381,7 +1401,9 @@ function _tfBadge(st){
|
|||||||
function rTfActions(){
|
function rTfActions(){
|
||||||
if(S.tfRunning)return'<button class="btn bsm" style="background:var(--rd);color:#fff;border-color:var(--rd);font-size:.72rem;padding:4px 12px" onclick="tfCancel()">Cancel</button>';
|
if(S.tfRunning)return'<button class="btn bsm" style="background:var(--rd);color:#fff;border-color:var(--rd);font-size:.72rem;padding:4px 12px" onclick="tfCancel()">Cancel</button>';
|
||||||
const st=S.tfStatus;let btns='';
|
const st=S.tfStatus;let btns='';
|
||||||
if(S.tfCode&&(st==='draft'||st==='failed'))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(S.tfCode&&(st==='draft'||st==='failed'||st==='destroyed'))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(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.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'||st==='failed')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==='planned'||st==='failed')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==='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')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==='applied')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>`;
|
||||||
@@ -1406,10 +1428,8 @@ function rTfTerminal(){
|
|||||||
label=S.tfDestroyOut?'Destroy Output':S.tfApplyOut?'Apply Output':'Plan Output';
|
label=S.tfDestroyOut?'Destroy Output':S.tfApplyOut?'Apply Output':'Plan Output';
|
||||||
color=S.tfDestroyOut?'#f38ba8':S.tfApplyOut?'#a6e3a1':'#cba6f7';
|
color=S.tfDestroyOut?'#f38ba8':S.tfApplyOut?'#a6e3a1':'#cba6f7';
|
||||||
}
|
}
|
||||||
return`<div class="tf-bc-right">
|
return`<div class="tf-term-hdr"><svg viewBox="0 0 16 16"><path d="M2 1h12a1 1 0 011 1v12a1 1 0 01-1 1H2a1 1 0 01-1-1V2a1 1 0 011-1zm1 3v9h10V4zm2 1l3 2.5L5 10z"/></svg><span style="color:${color}">${label}</span>${S.tfRunning?'<span style="margin-left:auto;font-size:.6rem;color:#f9e2af">● Executando...</span>':''}</div>
|
||||||
<div class="tf-term-hdr"><svg viewBox="0 0 16 16"><path d="M2 1h12a1 1 0 011 1v12a1 1 0 01-1 1H2a1 1 0 01-1-1V2a1 1 0 011-1zm1 3v9h10V4zm2 1l3 2.5L5 10z"/></svg><span style="color:${color}">${label}</span>${S.tfRunning?'<span style="margin-left:auto;font-size:.6rem;color:#f9e2af">● Executando...</span>':''}</div>
|
<div class="tf-term-body" id="tfTermOut">${out?escHtml(content):'<span style="opacity:.4">Aguardando execução...</span>'}</div>`;
|
||||||
<div class="tf-term-body" id="tfTermOut">${out?escHtml(content):'<span style="opacity:.4">Aguardando execução...</span>'}</div>
|
|
||||||
</div>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function rTfFiles(){
|
function rTfFiles(){
|
||||||
@@ -1468,6 +1488,31 @@ function rTfConfirmDestroy(){
|
|||||||
</div></div>`;
|
</div></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rTfConfirmRollback(){
|
||||||
|
return`<div class="tf-modal-overlay" onclick="S.tfConfirmRollback=false;R()"><div class="tf-modal" onclick="event.stopPropagation()">
|
||||||
|
<h3 style="display:flex;align-items:center;gap:.4rem"><svg viewBox="0 0 16 16" width="18" height="18" fill="#e67e22"><path d="M8 1a7 7 0 100 14A7 7 0 008 1zm0 2.5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 018 3.5zM8 10a1 1 0 100 2 1 1 0 000-2z"/></svg> Confirmar Rollback</h3>
|
||||||
|
<p style="font-size:.8rem;margin:.5rem 0">O apply falhou parcialmente. O rollback irá <strong>destruir todos os recursos</strong> que foram criados antes do erro.</p>
|
||||||
|
<p style="font-size:.74rem;color:var(--t3);margin-bottom:.8rem">Digite <strong>ROLLBACK</strong> para confirmar:</p>
|
||||||
|
<input type="text" id="tfRollbackInput" placeholder="ROLLBACK" style="margin-bottom:.6rem">
|
||||||
|
<div style="display:flex;gap:.5rem;justify-content:flex-end">
|
||||||
|
<button class="btn bs" onclick="S.tfConfirmRollback=false;R()">Cancelar</button>
|
||||||
|
<button class="btn bd" onclick="tfDoRollback()">Rollback</button>
|
||||||
|
</div>
|
||||||
|
</div></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tfDoRollback(){
|
||||||
|
const inp=document.getElementById('tfRollbackInput');
|
||||||
|
if(!inp||inp.value!=='ROLLBACK'){alert('Digite ROLLBACK para confirmar.');return}
|
||||||
|
S.tfConfirmRollback=false;R();
|
||||||
|
if(!S.tfWs)return;
|
||||||
|
try{
|
||||||
|
await $api('/terraform/workspaces/'+S.tfWs+'/destroy',{method:'POST',body:{confirm:'DESTROY'}});
|
||||||
|
S.tfStatus='destroying';S.tfRunning=true;S.tfDestroyOut='';S.tfBtab='plan';R();
|
||||||
|
await tfPollExec();
|
||||||
|
}catch(e){alert('Erro ao iniciar rollback: '+e.message)}
|
||||||
|
}
|
||||||
|
|
||||||
function tfStartResize(e){
|
function tfStartResize(e){
|
||||||
e.preventDefault();const el=document.getElementById('tfBottom');if(!el)return;
|
e.preventDefault();const el=document.getElementById('tfBottom');if(!el)return;
|
||||||
const startY=e.clientY;const startH=el.offsetHeight;
|
const startY=e.clientY;const startH=el.offsetHeight;
|
||||||
@@ -1595,7 +1640,7 @@ function tfSwitchBtab(tab){
|
|||||||
S.tfBtab=tab;S.tfEditIdx=-1;
|
S.tfBtab=tab;S.tfEditIdx=-1;
|
||||||
if(tab==='resources'&&!S.tfResources&&S.tfCompartment)tfLoadResources();
|
if(tab==='resources'&&!S.tfResources&&S.tfCompartment)tfLoadResources();
|
||||||
// Update only bottom panel content + tab active states
|
// Update only bottom panel content + tab active states
|
||||||
const left=document.querySelector('.tf-bc-left');if(left)left.innerHTML=rTfBottomContent();
|
const left=document.querySelector('.tf-bottom-content');if(left)left.innerHTML=rTfBottomContent();
|
||||||
const tabs=document.querySelectorAll('.tf-btab');tabs.forEach(t=>{const isActive=t.textContent.trim().toLowerCase().startsWith(tab);t.classList.toggle('active',isActive)});
|
const tabs=document.querySelectorAll('.tf-btab');tabs.forEach(t=>{const isActive=t.textContent.trim().toLowerCase().startsWith(tab);t.classList.toggle('active',isActive)});
|
||||||
// Update actions
|
// Update actions
|
||||||
const acts=document.querySelector('.tf-bottom-actions');if(acts){acts.innerHTML=(S.tfFiles.length?'<button class="btn bs bsm" onclick="tfDlAll()" style="font-size:.66rem;padding:2px 8px">Download All</button>':'')+rTfActions()}
|
const acts=document.querySelector('.tf-bottom-actions');if(acts){acts.innerHTML=(S.tfFiles.length?'<button class="btn bs bsm" onclick="tfDlAll()" style="font-size:.66rem;padding:2px 8px">Download All</button>':'')+rTfActions()}
|
||||||
@@ -1606,7 +1651,7 @@ function tfTogglePanel(p){S.tfPanel=S.tfPanel===p?'':p;R()}
|
|||||||
async function tfClear(){
|
async function tfClear(){
|
||||||
if(S.tfWs)try{await $api('/terraform/workspaces/'+S.tfWs,{method:'DELETE'})}catch(e){}
|
if(S.tfWs)try{await $api('/terraform/workspaces/'+S.tfWs,{method:'DELETE'})}catch(e){}
|
||||||
if(S.tfSid)try{await $api('/chat/'+S.tfSid,{method:'DELETE'})}catch(e){}
|
if(S.tfSid)try{await $api('/chat/'+S.tfSid,{method:'DELETE'})}catch(e){}
|
||||||
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.tfConfirmDest=false;S.tfComps=[];S.tfCompartment='';S.tfResources=null;S.tfResLoading=false;R()
|
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.tfComps=[];S.tfCompartment='';S.tfResources=null;S.tfResLoading=false;R()
|
||||||
}
|
}
|
||||||
function tfScroll(){setTimeout(()=>{const e=document.getElementById('tfchm');if(e)e.scrollTop=e.scrollHeight},50)}
|
function tfScroll(){setTimeout(()=>{const e=document.getElementById('tfchm');if(e)e.scrollTop=e.scrollHeight},50)}
|
||||||
function copyTfCode(){navigator.clipboard.writeText(S.tfCode).then(()=>alert('Código copiado!'))}
|
function copyTfCode(){navigator.clipboard.writeText(S.tfCode).then(()=>alert('Código copiado!'))}
|
||||||
@@ -1623,7 +1668,7 @@ async function tfSend(){
|
|||||||
let finalMsg=m;
|
let finalMsg=m;
|
||||||
if(S.tfFiles.length>0){
|
if(S.tfFiles.length>0){
|
||||||
const filesCtx=S.tfFiles.map(f=>'// filename: '+f.name+'\n'+f.content).join('\n\n');
|
const filesCtx=S.tfFiles.map(f=>'// filename: '+f.name+'\n'+f.content).join('\n\n');
|
||||||
finalMsg+='\n\n---\nARQUIVOS TERRAFORM ATUAIS NO WORKSPACE (corrija estes arquivos):\n```hcl\n'+filesCtx+'\n```';
|
finalMsg+='\n\n---\nARQUIVOS TERRAFORM ATUAIS NO WORKSPACE:\n```hcl\n'+filesCtx+'\n```\n\n⚠️ REGRA OBRIGATÓRIA: Gere APENAS os arquivos que precisam de correção. NÃO regenere arquivos sem erro. Se o erro está em um arquivo específico, retorne SOMENTE esse arquivo com `// filename:`. Arquivos não mencionados na sua resposta serão MANTIDOS como estão.';
|
||||||
}
|
}
|
||||||
if(S.tfPlanOut){finalMsg+='\n\n---\nÚLTIMO OUTPUT DO TERRAFORM PLAN:\n```\n'+S.tfPlanOut.slice(-3000)+'\n```'}
|
if(S.tfPlanOut){finalMsg+='\n\n---\nÚLTIMO OUTPUT DO TERRAFORM PLAN:\n```\n'+S.tfPlanOut.slice(-3000)+'\n```'}
|
||||||
if(S.tfApplyOut){finalMsg+='\n\n---\nÚLTIMO OUTPUT DO TERRAFORM APPLY:\n```\n'+S.tfApplyOut.slice(-3000)+'\n```'}
|
if(S.tfApplyOut){finalMsg+='\n\n---\nÚLTIMO OUTPUT DO TERRAFORM APPLY:\n```\n'+S.tfApplyOut.slice(-3000)+'\n```'}
|
||||||
@@ -1679,7 +1724,7 @@ async function tfApply(){
|
|||||||
if(!S.tfWs){alert('Execute Plan primeiro.');return}
|
if(!S.tfWs){alert('Execute Plan primeiro.');return}
|
||||||
try{
|
try{
|
||||||
await $api('/terraform/workspaces/'+S.tfWs+'/apply',{method:'POST'});
|
await $api('/terraform/workspaces/'+S.tfWs+'/apply',{method:'POST'});
|
||||||
S.tfStatus='applying';S.tfRunning=true;S.tfApplyOut='';S.tfBtab='plan';R();
|
S.tfStatus='applying';S.tfRunning=true;S.tfHadApply=true;S.tfApplyOut='';S.tfBtab='plan';R();
|
||||||
await tfPollExec();
|
await tfPollExec();
|
||||||
}catch(e){alert('Erro: '+e.message)}
|
}catch(e){alert('Erro: '+e.message)}
|
||||||
}
|
}
|
||||||
@@ -1702,6 +1747,37 @@ async function tfCancel(){
|
|||||||
S.tfRunning=false;S.tfStatus='failed';R();
|
S.tfRunning=false;S.tfStatus='failed';R();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function tfAutoFix(){
|
||||||
|
if(!S.tfPlanOut||!S.tfFiles.length||!S.tfModel)return;
|
||||||
|
// 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 errCtx=errLines.length?errLines.join('\n'):S.tfPlanOut.slice(-2000);
|
||||||
|
// Build auto-fix message
|
||||||
|
const filesCtx=S.tfFiles.map(f=>'// filename: '+f.name+'\n'+f.content).join('\n\n');
|
||||||
|
const fixMsg='O terraform plan falhou com os seguintes erros. Corrija SOMENTE os arquivos afetados.\n\n---\nERROS DO PLAN:\n```\n'+errCtx+'\n```\n\n---\nARQUIVOS TERRAFORM ATUAIS NO WORKSPACE:\n```hcl\n'+filesCtx+'\n```\n\n⚠️ REGRA OBRIGATÓRIA: Gere APENAS os arquivos que precisam de correção com `// filename:`. Arquivos não mencionados serão MANTIDOS como estão.';
|
||||||
|
// Show in chat
|
||||||
|
S.tfMsgs.push({r:'user',c:'🔧 Auto-corrigir erros do plan',t:tstamp()});
|
||||||
|
S.tfMsgs.push({r:'assistant',c:IC.wait+' _Analisando erros e corrigindo..._',thinking:true});R();tfScroll();
|
||||||
|
try{
|
||||||
|
const body={message:fixMsg,session_id:S.tfSid,use_tools:false};
|
||||||
|
if(S.tfModel.startsWith('cfg:'))body.genai_config_id=S.tfModel.slice(4);
|
||||||
|
else{
|
||||||
|
if(!S.tfOci){S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:IC.warn+' Selecione uma credencial OCI.',t:tstamp()});R();return}
|
||||||
|
body.model_id=S.tfModel;body.oci_config_id=S.tfOci;body.genai_region=S.tfRegion;body.compartment_id=S.tfCompartment;
|
||||||
|
}
|
||||||
|
const d=await $api('/terraform/chat',{method:'POST',body});
|
||||||
|
S.tfSid=d.session_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||'Sem resposta',t:tstamp()});R();tfScroll()}
|
||||||
|
// Auto re-plan after fix
|
||||||
|
if(S.tfFiles.length&&S.tfCode){
|
||||||
|
S.tfMsgs.push({r:'assistant',c:IC.wait+' _Re-executando plan..._',thinking:true});R();
|
||||||
|
await tfSaveAndPlan();
|
||||||
|
S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);R()
|
||||||
|
}
|
||||||
|
}catch(e){S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:IC.err+' Erro auto-fix: '+e.message,t:tstamp()});R()}
|
||||||
|
}
|
||||||
|
|
||||||
async function tfPollExec(){
|
async function tfPollExec(){
|
||||||
for(let i=0;i<1800;i++){
|
for(let i=0;i<1800;i++){
|
||||||
await new Promise(r=>setTimeout(r,2000));
|
await new Promise(r=>setTimeout(r,2000));
|
||||||
@@ -1712,7 +1788,7 @@ async function tfPollExec(){
|
|||||||
S.tfDestroyOut=r.destroy_output||S.tfDestroyOut;
|
S.tfDestroyOut=r.destroy_output||S.tfDestroyOut;
|
||||||
S.tfStatus=r.status;
|
S.tfStatus=r.status;
|
||||||
const done=!['planning','applying','destroying'].includes(r.status);
|
const done=!['planning','applying','destroying'].includes(r.status);
|
||||||
if(done)S.tfRunning=false;
|
if(done){S.tfRunning=false;if(r.status==='destroyed')S.tfHadApply=false;}
|
||||||
// Update only the terminal body without full re-render
|
// Update only the terminal body without full re-render
|
||||||
const t=document.getElementById('tfTermOut');
|
const t=document.getElementById('tfTermOut');
|
||||||
if(t){
|
if(t){
|
||||||
@@ -2369,7 +2445,7 @@ function rOci(){const eo=S.editing?.type==='oci'?S.ociCfg.find(c=>c.id===S.editi
|
|||||||
<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>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><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></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><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="ct">${eo?IC.edit+' Editar Credencial':IC.plus+' Nova Credencial'}</div>
|
||||||
@@ -2384,7 +2460,7 @@ ${!S.ociCfg.length?'<div class="emp" style="padding:.85rem"><p>Nenhuma credencia
|
|||||||
<div class="g2"><div class="ig"><label>Private Key (.pem)</label>${eo?'<div class="ht">Deixe vazio para manter a chave atual</div>':''}<input type="file" id="osk" accept=".pem"></div>
|
<div class="g2"><div class="ig"><label>Private Key (.pem)</label>${eo?'<div class="ht">Deixe vazio para manter a chave atual</div>':''}<input type="file" id="osk" accept=".pem"></div>
|
||||||
<div class="ig"><label>Public Key (.pem)</label><input type="file" id="opk" accept=".pem"></div></div>
|
<div class="ig"><label>Public Key (.pem)</label><input type="file" id="opk" accept=".pem"></div></div>
|
||||||
<div class="g2"><div class="ig"><label>Key Passphrase</label><div class="ht">Apenas se a chave privada for protegida por senha</div><input type="password" id="okp" placeholder="(opcional)"></div>
|
<div class="g2"><div class="ig"><label>Key Passphrase</label><div class="ht">Apenas se a chave privada for protegida por senha</div><input type="password" id="okp" placeholder="(opcional)"></div>
|
||||||
<div class="ig"></div></div>
|
<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;
|
||||||
@@ -2393,6 +2469,7 @@ async function sOci(){const tn=document.getElementById('otn').value;
|
|||||||
fd.append('user_ocid',document.getElementById('ouo').value);fd.append('fingerprint',document.getElementById('ofp').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('region',S.ociFormRegVal||'');fd.append('compartment_id',document.getElementById('ocp').value);
|
||||||
fd.append('key_passphrase',document.getElementById('okp').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 sk=document.getElementById('osk').files[0];
|
||||||
const isEdit=S.editing?.type==='oci';
|
const isEdit=S.editing?.type==='oci';
|
||||||
if(!isEdit){
|
if(!isEdit){
|
||||||
|
|||||||
Reference in New Issue
Block a user