diff --git a/README.md b/README.md
index 27c2fd7..d902110 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
-
+
@@ -23,7 +23,7 @@
AI Agent β Infrastructure & Security Engineer is a self-hosted web application that automates **CIS Oracle Cloud Infrastructure Foundations Benchmark 3.0** compliance checks, powered by **OCI Generative AI** for intelligent analysis and an **MCP (Model Context Protocol)** server architecture for extensible task execution.
-The platform combines security compliance scanning, AI-powered chat with **RAG (Retrieval-Augmented Generation)**, infrastructure exploration, and vector-based knowledge storage into a single, containerized solution with an **Oracle Dark Premium** theme (light/dark modes), **KPI dashboard** with compliance gauge, and **Chart.js** visualizations.
+The platform combines security compliance scanning, AI-powered chat with **RAG (Retrieval-Augmented Generation)**, infrastructure exploration, and vector-based knowledge storage into a single, containerized solution with a **dual frontend** (legacy SPA + React SPA at `/app/`), **Oracle Dark Premium** theme (light/dark modes), **KPI dashboard** with compliance gauge, **i18n** (pt/en), and **Chart.js** visualizations.
---
@@ -127,11 +127,26 @@ The platform combines security compliance scanning, AI-powered chat with **RAG (
- **Level 2**: Advanced security controls that may restrict functionality or require more effort to implement. Recommended for high-security environments.
- **Multiple output formats**: HTML summary, CSV per section/finding, JSON summary, optional XLSX
- **Report history**: full execution history with status, tenancy filter, and download actions (HTML/JSON) in the Reports tab
+- **Delete reports**: remove completed reports and their associated files from the system
- **File browser**: dedicated Downloads tab with expandable cards per completed report, files grouped by category (Summary, CIS Findings, OBP, Raw Data, etc.)
+- **Embed individual files**: embed any report file (CSV, TXT, JSON, PDF) directly into the ADB vector store for RAG enrichment
- Region filtering with multi-select
- Real-time progress tracking with phase-based progress bar
- **CIS Engine auto-update**: check for new versions of `cis_reports.py` from Oracle's GitHub repository and update with one click (admin only). Custom patches are automatically reapplied after update
+### π LAD A-Team CIS Compliance Report
+- **Professional compliance report** following Oracle Cloud Security Assessment format (cover page, purpose statement, disclaimer, copyright)
+- **Table of Contents** with links to each section and recommendation
+- **Security Overview**: 7 Oracle security pillars (Customer Isolation, Data Encryption, Data Management, etc.)
+- **CIS Assessment Summary**: table with Domains / Total Controls / Failed / Passed per section
+- **CIS Benchmark Recommendation Summary**: full table with Item / Description / Status (OK/FAILED)
+- **Detailed findings**: per-recommendation cards with compliance percentage bar, result description, and remediation steps
+- **RAG-powered remediation**: enriched remediation steps fetched from ADB vector store (`CISRECOM` table) using cosine similarity search β filters by exact recommendation number, extracts only remediation section from matched chunks
+- **Affected resources CSV links**: non-compliant findings show a clickable link to download the CSV with affected resource details (authenticated via JWT token)
+- **Download PDF**: `window.print()` with CSS `@page A4` formatting for professional PDF output
+- **Caching**: generated HTML cached to disk, subsequent views are instant; "Regenerar" button clears cache and re-generates
+- **Collapsible sections**: HTML report and compliance report start minimized, expandable on demand
+
### π‘οΈ Built-in CIS MCP Server (Granular Per-Section)
- **Auto-registered** CIS Compliance Scanner MCP server β available out of the box
- **12 granular tools** instead of monolithic full-tenancy scan:
@@ -418,16 +433,27 @@ Allow group to read buckets in compartment
```
oci-cis-agent/
βββ backend/
-β βββ app.py # FastAPI application (~5900 lines)
+β βββ app.py # FastAPI application (~6700 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 (~3200 lines)
+β βββ index.html # Legacy SPA with Oracle Dark Premium theme (~2800 lines)
+βββ frontend-react/
+β βββ src/ # React 19 SPA (TypeScript, 42 source files, ~12500 lines)
+β β βββ pages/ # 8 page components (Dashboard, Explorer, Reports, Chat, Terraform, etc.)
+β β βββ api/ # API client + endpoint modules
+β β βββ stores/ # Zustand auth store
+β β βββ hooks/ # Custom hooks (theme, i18n)
+β β βββ i18n/ # Internationalization (pt/en, 625 keys)
+β βββ dist/ # Built SPA served at /app/
+βββ scripts/
+β βββ reportgenerator.js # n8n reference script (report layout)
+β βββ *.pdf # Oracle CIS report PDF template
βββ nginx/
-β βββ default.conf # Reverse proxy config
+β βββ default.conf # Reverse proxy (legacy / + React /app/ + API /api/)
βββ docker-compose.yml # Orchestration
βββ logo.svg # Project logo (Oracle AI Robot)
βββ .env.example # Environment template
@@ -562,6 +588,7 @@ oci-cis-agent/
|--------|----------|-------------|
| GET | `/api/embeddings/preview/{rid}` | Preview report chunks before embedding (with tenancy/regions/compartments) |
| POST | `/api/embeddings/report/{rid}` | Generate embeddings from CIS report (chunked by section, accepts `table_name`, `report_date`) |
+| POST | `/api/embeddings/report/{rid}/file/{fid}` | Embed individual report file (CSV/TXT/JSON/PDF) into vector store |
| POST | `/api/embeddings/upload` | Upload file (.txt/.pdf/.csv/.json/.md) and generate embeddings |
| POST | `/api/embeddings/upload-url` | Import URL (web page or PDF), extract text, and generate embeddings |
| POST | `/api/embeddings/consult` | Query embeddings via vector search + GenAI (natural language Q&A) |
@@ -601,11 +628,15 @@ oci-cis-agent/
| DELETE | `/api/chat/{sid}` | Delete chat session and messages |
| POST | `/api/reports/run` | Execute CIS report |
| GET | `/api/reports` | List reports |
+| DELETE | `/api/reports/{rid}` | Delete report and associated files |
| GET | `/api/reports/{id}/html` | View HTML report |
| GET | `/api/reports/{id}/download` | Download report |
| GET | `/api/reports/{rid}/summary` | Report KPI summary (score, pass/fail, sections) |
| GET | `/api/reports/{rid}/files` | List report files by category |
| GET | `/api/reports/{rid}/files/{fid}/download` | Download individual report file |
+| GET | `/api/reports/{rid}/compliance-report` | Serve cached LAD A-Team compliance report HTML |
+| POST | `/api/reports/{rid}/compliance-report/generate` | Generate compliance report (background, with RAG) |
+| GET | `/api/reports/{rid}/compliance-report/status` | Check if compliance report is ready |
### CIS Engine
@@ -680,7 +711,7 @@ All models include OCID mapping for `us-ashburn-1`. For other regions, use the "
| Component | Technology |
|-----------|-----------|
| Backend | Python 3.12, FastAPI 0.115, Uvicorn |
-| Frontend | Vanilla JS SPA, Oracle Dark Premium theme, Chart.js |
+| Frontend | React 19 SPA (Vite + TypeScript) at `/app/` + legacy Vanilla JS SPA, Oracle Dark Premium theme, Chart.js, i18n (pt/en) |
| Auth | JWT + TOTP MFA + RBAC |
| Database | SQLite (WAL mode) |
| OCI SDK | `oci` 2.133.0, `oci-cli` |
@@ -697,6 +728,7 @@ All models include OCID mapping for `us-ashburn-1`. For other regions, use the "
| Version | Date | Changes |
|---------|------|---------|
+| **v2.7** | 2026-03 | **LAD A-Team CIS Compliance Report**: professional Oracle-format compliance report with cover page, TOC, Security Overview (7 pillars), CIS Assessment Summary, detailed findings with compliance % bars, RAG-powered remediation from ADB vector store (`CISRECOM` table with strict recommendation number filtering), affected resources CSV download links per non-compliant finding (JWT auth via query param). **React SPA**: 16-page React 19 frontend at `/app/` (TypeScript, Vite, Zustand, 42 source files), dual-served alongside legacy SPA via nginx. **i18n**: full internationalization with 625 keys (pt/en), language switcher persisted in localStorage. **Report management**: delete reports endpoint, embed individual report files (CSV/TXT/JSON/PDF) into vector store. **Terraform ZIP download**: replaced individual file downloads with client-side ZIP generation (pure JS, no external lib). **Explorer UX**: silent polling during start/stop actions eliminates flickering (resources update in-place without clearing the list). **Report sections collapsed by default**: HTML report and compliance report iframes start minimized. |
| **v2.6** | 2026-03 | **Rename**: app renamed to "AI Agent β Infrastructure & Security Engineer". **Session Token Auth**: OCI session token authentication support (type selector API Key/Session Token, conditional form fields, collapsible credential form, type tags in credentials table, `SecurityTokenSigner` backend). **Terraform Workspaces submenu**: sidebar sub-tab listing all workspaces grouped by date with accordion UI, filtered by existing chat sessions (INNER JOIN), actions (plan/apply/destroy/delete/open). **Retry on failure**: "Reenviar" button on failed messages across Chat Agent, Terraform Agent, and Prompt Generator. **Terraform fixes**: `f2` undefined variable in dedup logic, `oci_core_services` false positive validation (data sources excluded), `S.tfCode` reconstruction from `S.tfFiles` for Re-plan, auto-fix button visible without pre-selected model, DRG attachment rules in system prompt. **Resilience**: stuck workspace recovery on container restart (planning/applying/destroying β reset + lock file cleanup). |
| **v2.5** | 2026-03 | **Explorer Expansion**: KPI stats bar with per-category resource counts (cached in SQLite `explorer_counts_cache`, background refresh on compartment access), enhanced views for VCNs/Subnets/Load Balancers/Buckets with expandable cards, start/stop for DB Systems + MySQL + Container Instances (5 resource types total), dead state filtering (`TERMINATED`/`DELETED`/`DELETING` hidden across all 36 endpoints), wider compartment tree (280px), IAM excluded from compartment totals. **Prompt Generator Intelligence**: same knowledge pipeline as Terraform Agent β auto-detects resource types from user message, fetches official docs (Example Usage + Arguments) from GitHub cached in SQLite, uses compact categorized resource reference. **Network Firewall fix**: `NetworkFirewallPolicySummaryCollection` not iterable β added `.items` accessor for collection objects. |
| **v2.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 |
diff --git a/backend/app.py b/backend/app.py
index 29d16cc..915f517 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -17,7 +17,7 @@ from fastapi import (
Query, Body, BackgroundTasks
)
from fastapi.middleware.cors import CORSMiddleware
-from fastapi.responses import JSONResponse, FileResponse, StreamingResponse
+from fastapi.responses import JSONResponse, FileResponse, StreamingResponse, HTMLResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
import jwt as pyjwt
@@ -3566,6 +3566,38 @@ async def embed_report(rid: str, req: dict, bg: BackgroundTasks, u=Depends(requi
_audit(u["id"], u["username"], "embed_report", rid, f"{label}, tenancy={tenancy}")
return {"ok": True, "message": f"Embedding de {len(documents)} seΓ§Γ£o(Γ΅es) iniciado (tenancy: {tenancy})", "sections": len(documents), "tenancy": tenancy}
+@app.post("/api/embeddings/report/{rid}/file/{fid}")
+async def embed_report_file(rid: str, fid: str, req: dict, bg: BackgroundTasks, u=Depends(require("admin","user"))):
+ """Embed a specific report file (CSV, JSON, TXT, etc.) into ADB vector store."""
+ vid = req.get("adb_config_id")
+ if not vid: raise HTTPException(400, "adb_config_id is required")
+ with db() as c:
+ r = c.execute("SELECT tenancy_name, config_id FROM reports WHERE id=? AND status='completed'", (rid,)).fetchone()
+ if not r: raise HTTPException(404, "Report not found or not completed")
+ f = c.execute("SELECT * FROM report_files WHERE id=? AND report_id=?", (fid, rid)).fetchone()
+ if not f: raise HTTPException(404, "File not found")
+ p = Path(f["file_path"])
+ if not p.exists(): raise HTTPException(404, "File not found on disk")
+ fname = f["file_name"].lower()
+ allowed = ('.txt', '.csv', '.json', '.md', '.pdf')
+ if not any(fname.endswith(ext) for ext in allowed):
+ raise HTTPException(400, f"Formatos aceitos para embedding: {', '.join(allowed)}")
+ raw = p.read_bytes()
+ if fname.endswith('.pdf'):
+ content = _extract_pdf_text(raw)
+ else:
+ content = raw.decode("utf-8", errors="replace")
+ if not content.strip(): raise HTTPException(400, "File is empty")
+ documents = _chunk_text_file(content, f["file_name"])
+ if not documents: raise HTTPException(400, "No content chunks found")
+ cfg, gc = _get_adb_and_genai(vid, oci_config_id=r.get("config_id"))
+ target_table = req.get("table_name") or None
+ tenancy = r["tenancy_name"] or "unknown"
+ bg.add_task(_ingest_documents_task, cfg, gc, documents, u["id"], u["username"],
+ table_name=target_table, tenancy=tenancy)
+ _audit(u["id"], u["username"], "embed_report_file", f"{rid}/{fid}", f"{f['file_name']}, {len(documents)} chunks, tenancy={tenancy}")
+ return {"ok": True, "message": f"Embedding de {f['file_name']} iniciado ({len(documents)} chunks)", "chunks": len(documents)}
+
def _extract_pdf_text(file_bytes: bytes) -> str:
"""Extract text from a PDF file using PyPDF2 or pdfplumber."""
try:
@@ -3983,6 +4015,23 @@ async def get_report(rid, u=Depends(current_user)):
if u["role"]!="admin" and r["user_id"]!=u["id"]: raise HTTPException(403)
return dict(r)
+@app.delete("/api/reports/{rid}")
+async def delete_report(rid: str, u=Depends(current_user)):
+ with db() as c:
+ r = c.execute("SELECT id, user_id, status, json_path, html_path FROM reports WHERE id=?", (rid,)).fetchone()
+ if not r: raise HTTPException(404)
+ if u["role"] != "admin" and r["user_id"] != u["id"]: raise HTTPException(403)
+ if r["status"] == "running": raise HTTPException(400, "Cannot delete a running report")
+ # Remove associated files
+ for path_col in ("json_path", "html_path"):
+ p = r[path_col]
+ if p and Path(p).exists():
+ try: Path(p).unlink()
+ except OSError: pass
+ c.execute("DELETE FROM reports WHERE id=?", (rid,))
+ _audit(u["id"], u["username"], "delete_report", rid)
+ return {"ok": True}
+
@app.get("/api/reports/{rid}/summary")
async def report_summary(rid: str, u=Depends(current_user)):
with db() as c:
@@ -4064,6 +4113,689 @@ async def report_html(rid):
if not r or not r["html_path"] or not Path(r["html_path"]).exists(): raise HTTPException(404)
return FileResponse(r["html_path"], media_type="text/html")
+
+# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+# LAD A-Team CIS Compliance Report
+# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+def _extract_section(text: str, section_name: str) -> str:
+ """Extract a named section from a CIS Benchmark chunk (Description, Remediation, etc.)."""
+ import re as _re
+ pattern = _re.compile(r'\n' + _re.escape(section_name) + r'[:\s]*\n', _re.IGNORECASE)
+ match = pattern.search(text)
+ if not match:
+ return ""
+ start = match.end()
+ end_match = _re.search(
+ r'\n(?:Description|Rationale|Audit|Remediation|Additional Information|CIS Controls|Controls\s*\nVersion|References|Default Value|Impact|Recommendation|Profile Applicability)[:\s]*\n',
+ text[start:], _re.IGNORECASE
+ )
+ if end_match:
+ return text[start:start + end_match.start()].strip()
+ return text[start:].strip()
+
+
+def _extract_remediation_section(text: str) -> str:
+ """Extract only the Remediation section."""
+ return _extract_section(text, "Remediation")
+
+
+def _extract_description_section(text: str) -> str:
+ """Extract the Description section (what this CIS finding is about)."""
+ return _extract_section(text, "Description")
+
+
+def _format_remediation_html(text: str) -> str:
+ """Format remediation text with code snippets and proper indentation."""
+ import re as _re
+ if not text:
+ return ""
+
+ def esc(v):
+ return (v or "").replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
+
+ lines = text.split('\n')
+ html_parts = []
+ in_code = False
+ code_buf = []
+
+ for line in lines:
+ stripped = line.strip()
+ # Detect CLI commands and policy statements
+ is_code = (
+ stripped.startswith('oci ') or
+ stripped.startswith('Allow ') or
+ stripped.startswith('Define ') or
+ stripped.startswith('Endorse ') or
+ stripped.startswith('Admit ') or
+ stripped.startswith('terraform ') or
+ stripped.startswith('curl ') or
+ stripped.startswith('openssl ') or
+ stripped.startswith('ssh ') or
+ stripped.startswith('python') or
+ stripped.startswith('$ ') or
+ stripped.startswith('# ') and not stripped.startswith('# ') and len(stripped) < 5 or
+ (in_code and stripped and not stripped[0].isupper() and not stripped.startswith('http'))
+ )
+
+ if is_code:
+ if not in_code:
+ in_code = True
+ code_buf = []
+ code_buf.append(stripped)
+ else:
+ if in_code and code_buf:
+ html_parts.append('' + esc('\n'.join(code_buf)) + ' ')
+ code_buf = []
+ in_code = False
+ if stripped:
+ # Numbered steps
+ step_match = _re.match(r'^(\d+)\.\s+(.+)', stripped)
+ if step_match:
+ html_parts.append(f'{step_match.group(1)}. {esc(step_match.group(2))}
')
+ else:
+ html_parts.append(f'{esc(stripped)}
')
+
+ # Flush remaining code
+ if code_buf:
+ html_parts.append('' + esc('\n'.join(code_buf)) + ' ')
+
+ return '\n'.join(html_parts)
+
+
+def _search_cis_remediation(title: str, rec_num: str, user_id: str) -> dict:
+ """Search CIS Recommendations in vector DB. Returns {'description': str, 'remediation': str}."""
+ try:
+ with db() as c:
+ adb_cfgs = c.execute(
+ "SELECT * FROM adb_vector_configs WHERE user_id=? AND is_active=1", (user_id,)
+ ).fetchall()
+ if not adb_cfgs:
+ return {"description": "", "remediation": ""}
+ query = f"Recommendation: {rec_num} {title} Remediation Audit From Console From Command Line"
+ for adb_cfg in adb_cfgs:
+ adb_cfg = dict(adb_cfg)
+ try:
+ # Resolve embedding config: use the ADB's own embedding model
+ emb_model = adb_cfg.get("embedding_model_id", "cohere.embed-v4.0")
+
+ tables = _get_tables_for_config(adb_cfg["id"], active_only=True)
+ if not tables:
+ continue
+
+ # Resolve GenAI config for embedding generation
+ genai_linked = None
+ if adb_cfg.get("genai_config_id"):
+ with db() as c:
+ row = c.execute("SELECT * FROM genai_configs WHERE id=?", (adb_cfg["genai_config_id"],)).fetchone()
+ if row: genai_linked = dict(row)
+ emb_genai = _resolve_embed_config(genai_cfg=genai_linked)
+
+ # Search across cisrecom + section-specific tables
+ all_docs = []
+ search_tables = [t["table_name"] for t in tables if "cisrecom" in t["table_name"].lower()]
+ if not search_tables:
+ search_tables = [t["table_name"] for t in tables]
+
+ for tbl_name in search_tables:
+ try:
+ # Detect dimension per table
+ tbl_model = emb_model
+ try:
+ actual_dim = _get_table_embedding_dim(adb_cfg, tbl_name)
+ if actual_dim and actual_dim in _DIM_TO_MODEL:
+ tbl_model = _DIM_TO_MODEL[actual_dim]
+ except Exception:
+ pass
+ query_embedding = _embed_text(query, emb_genai, tbl_model)
+ docs = _vector_search(adb_cfg, query_embedding, top_k=10, table_name=tbl_name)
+ if docs:
+ all_docs.extend(docs)
+ except Exception:
+ pass
+
+ if all_docs:
+ import re as _re
+ all_docs.sort(key=lambda d: d.get("distance", 999))
+ # STRICT FILTER: only keep chunks that mention THIS exact recommendation number
+ exact_pattern = _re.compile(
+ r'(?:Recommendation[:\s]*' + _re.escape(rec_num) + r'|^' + _re.escape(rec_num) + r')\s',
+ _re.MULTILINE | _re.IGNORECASE
+ )
+ matched = [d["content"] for d in all_docs
+ if d.get("content") and len(d["content"]) > 500
+ and exact_pattern.search(d["content"])]
+ if matched:
+ best = matched[0]
+ desc = _extract_description_section(best)
+ rem = _extract_remediation_section(best)
+ # Try second chunk if first has no remediation
+ if not rem and len(matched) > 1:
+ rem = _extract_remediation_section(matched[1])
+ return {"description": desc, "remediation": rem or best}
+ except Exception as e:
+ log.warning(f"CIS remediation search failed for {adb_cfg.get('config_name','?')}: {e}")
+ return {"description": "", "remediation": ""}
+ except Exception as e:
+ log.warning(f"CIS remediation search error: {e}")
+ return {"description": "", "remediation": ""}
+
+
+def _build_compliance_html(filtered_rows: list, tenancy_name: str, extract_date: str, sections_data: dict, rid: str = "", file_id_map: dict = None) -> str:
+ """Build the full LAD A-Team CIS Compliance Report HTML."""
+ import re as _re
+ from datetime import datetime as _dt
+
+ now = _dt.utcnow()
+ months_pt = ["Janeiro","Fevereiro","MarΓ§o","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"]
+ month_year = f"{months_pt[now.month - 1]}, {now.year}"
+
+ def esc(v): return (v or "").replace("&","&").replace("<","<").replace(">",">").replace('"',""")
+ def slug(s): return _re.sub(r"[^\w-]","", _re.sub(r"\s+","-",(s or "").strip().lower()))
+
+ total_all = len(filtered_rows)
+ total_passed = sum(1 for r in filtered_rows if r["__compliant"])
+ total_failed = total_all - total_passed
+ pct_global = round(total_passed / total_all * 100) if total_all else 0
+
+ # ββ Domains table rows ββ
+ domains_rows = ""
+ for sec_name, sd in sections_data.items():
+ domains_rows += f'{esc(sec_name)} {sd["total"]} {sd["failed"]} {sd["passed"]} '
+
+ # ββ CIS Benchmark Summary table ββ
+ summary_rows = ""
+ current_section = ""
+ for r in filtered_rows:
+ sec = r["Section"]
+ rec = r.get("Recommendation #", "")
+ title = r.get("Title", "")
+ is_compliant = r["__compliant"]
+ if sec != current_section:
+ current_section = sec
+ summary_rows += f''
+ status_cls = "pass" if is_compliant else "fail"
+ status_txt = "OK" if is_compliant else "FAILED"
+ summary_rows += f'{esc(rec)} {esc(title)} {status_txt} '
+
+ # ββ Detailed sections ββ
+ sections_html = ""
+ for sec_name, sd in sections_data.items():
+ sec_id = f"section-{slug(sec_name)}"
+ sections_html += f''
+ sections_html += f'
{esc(sec_name)} '
+
+ for r in sd["items"]:
+ rec = r.get("Recommendation #", "")
+ title = r.get("Title", "")
+ rec_id = f"rec-{slug(sec_name)}-{slug(rec)}"
+ is_compliant = r["__compliant"]
+ findings = r.get("Findings", "").strip()
+ findings_num = r["__findings"]
+ total_items = r.get("Total", "").strip()
+ compliant_items = r.get("Compliant Items", "").strip()
+ pct = r["__pct"]
+ csv_remediation = (r.get("Remediation", "") or "").strip()
+ rag_data = r.get("__rag_remediation", {})
+ if isinstance(rag_data, str):
+ rag_data = {"description": "", "remediation": rag_data}
+ rag_desc = (rag_data.get("description", "") or "").strip()
+ rag_rem = (rag_data.get("remediation", "") or "").strip()
+
+ # Status line
+ if is_compliant:
+ result_html = f'
Result: Tenancy {esc(tenancy_name)}: Compliant
'
+ else:
+ detail = ""
+ if findings_num is not None and total_items:
+ detail = f" ({findings_num} of {esc(total_items)} items)"
+ result_html = f'
Result: Tenancy {esc(tenancy_name)}: Non-Compliant{detail}
'
+
+ # Description (what this CIS finding is about)
+ description_html = ""
+ if not is_compliant and rag_desc:
+ description_html = f'
Description: {esc(rag_desc)}
'
+
+ # Remediation (only for non-compliant) β formatted with code snippets
+ remediation_html = ""
+ if not is_compliant:
+ rem_text = rag_rem or csv_remediation
+ if rem_text:
+ formatted = _format_remediation_html(rem_text)
+ remediation_html = f'
'
+
+ # Compliance bar
+ bar_html = ""
+ if pct is not None:
+ cls = "bad" if pct <= 20 else "warn" if pct <= 60 else "ok"
+ bar_html = f'
'
+
+ # Findings file link
+ findings_file_html = ""
+ csv_filename = (r.get("Filename", "") or "").strip()
+ if csv_filename and not is_compliant and file_id_map:
+ fid = file_id_map.get(csv_filename, "")
+ if fid and rid:
+ dl_url = f"/api/reports/{rid}/files/{fid}/download"
+ findings_file_html = f'
'
+
+ sections_html += f'''
+
+
{esc(rec)} {esc(title)}
+ {bar_html}
+ {result_html}
+ {findings_file_html}
+ {description_html}
+ {remediation_html}
+ '''
+
+ sections_html += '
'
+
+ # ββ TOC ββ
+ toc_lines = ''
+ toc_lines += ''
+ for sec_name, sd in sections_data.items():
+ sec_id = f"section-{slug(sec_name)}"
+ toc_lines += f''
+ for r in sd["items"]:
+ rec = r.get("Recommendation #", "-")
+ title = r.get("Title", "-")
+ rec_id = f"rec-{slug(sec_name)}-{slug(rec)}"
+ toc_lines += f''
+
+ PURPOSE_TEXT = "The purpose of the cloud security assessment is to comprehensively evaluate the organization's security posture in its cloud environment, identifying vulnerabilities and gaps that could compromise data integrity, confidentiality, and availability. This assessment aims to enhance information security maturity, strengthen defenses against cyber threats, ensure compliance with regulations, and foster a culture of security awareness. Through periodic assessments, the organization proactively mitigates risks, ensuring continuous security for cloud-configured assets and maintaining stakeholders' trust in data protection capabilities."
+ DISCLAIMER_TEXT = "This document in any form, software or printed matter, contains proprietary information that is the exclusive property of Oracle. Your access to and use of this confidential material is subject to the terms and conditions of your Oracle software license and service agreement, which has been executed and with which you agree to comply. This document and information contained herein may not be disclosed, copied, reproduced or distributed to anyone outside Oracle without prior written consent of Oracle. This document is not part of your license agreement, nor can it be incorporated into any contractual agreement with Oracle or its subsidiaries or affiliates.\n\nThis document is for informational purposes only and is intended solely to assist you in planning for the implementation and upgrade of the product features described. It is not a commitment to deliver any material, code, or functionality, and should not be relied upon in making purchasing decisions."
+ SECURITY_OVERVIEW = """Oracle's mission is to build cloud infrastructure and platform services for your business to have effective and manageable security to run your mission-critical workloads and store your data with confidence. Oracle Cloud Infrastructure's security approach is based on seven core pillars:
+
+Customer Isolation β Allow customers to deploy their application and data assets in an environment that commits full isolation from other tenants and Oracle's staff.
+
+Data Encryption β Protect customer data at-rest and in-transit in a way that allows customers to meet their security and compliance requirements for cryptographic algorithms and key management.
+
+Security Controls β Offer customers effective and easy-to-use security management solutions that allow them to constrain access to their services and segregate operational responsibilities.
+
+Visibility β Offer customers comprehensive log data and security analytics that they can use to audit and monitor actions on their resources.
+
+Secure Hybrid Cloud β Enable customers to use their existing security assets, such as user accounts and policies, as well as third-party security solutions when accessing their cloud resources.
+
+High Availability β Offer fault-independent data centers that enable high availability scale-out architectures and are resilient against network attacks.
+
+Verifiably Secure Infrastructure β Follow rigorous processes and use effective security controls in all phases of cloud service development and operation."""
+
+ return f'''
+
+
+
+LAD A-Team CIS Compliance Report β {esc(tenancy_name)}
+
+
+
+
+
+
+
+
ORACLE
+
+
Oracle Cloud Security Assessment
+
{esc(tenancy_name)} β Oracle Cloud Infrastructure
+
+
+
+
+
+
Purpose Statement
+
{esc(PURPOSE_TEXT)}
+
+
+
Disclaimer
+
{esc(DISCLAIMER_TEXT)}
+
+
+
+
+
ORACLE
+
Table of contents
+
+
{toc_lines}
+
+
+
+
+
Security Overview
+
{SECURITY_OVERVIEW}
+
+
+
+
+
CIS Security Assessment
+
CIS Oracle Cloud Infrastructure Foundations Benchmark v3.0.0
+
Number of controls per domain and non-compliant controls on tenancy {esc(tenancy_name)} :
+
+ DOMAINS TOTAL CONTROLS FAILED PASSED
+ {domains_rows}
+
+
Total: {total_all} • Passed: {total_passed} • Failed: {total_failed} • Compliance: {pct_global}%
+
+
+
+
+
CIS Benchmark Recommendation Summary
+
Status per recommendation
+
+ Item Description Status
+ {summary_rows}
+
+
+
+
+{sections_html}
+
+
+
+
+🖶 Download PDF
+
+
+
+'''
+
+
+def _generate_compliance_report(rid: str, user_id: str, force_rag: bool = True) -> str:
+ """Generate and cache the LAD A-Team CIS Compliance Report HTML."""
+ import csv as csvmod
+ import re as _re
+
+ with db() as c:
+ report = c.execute("SELECT * FROM reports WHERE id=?", (rid,)).fetchone()
+ if not report:
+ raise HTTPException(404, "Report not found")
+
+ rdir = REPORTS / rid
+ csv_path = rdir / "cis_summary_report.csv"
+ if not csv_path.exists():
+ raise HTTPException(404, "cis_summary_report.csv not found")
+
+ rows = []
+ with open(csv_path, "r", encoding="utf-8") as f:
+ for row in csvmod.DictReader(f):
+ rows.append(row)
+ if not rows:
+ raise HTTPException(404, "No data in summary CSV")
+
+ tenancy_name = report["tenancy_name"] or "Unknown"
+ extract_date = rows[0].get("extract_date", "") if rows else ""
+
+ # Build filename β file_id map for download links
+ file_id_map = {}
+ with db() as c:
+ rf = c.execute("SELECT id, file_name FROM report_files WHERE report_id=?", (rid,)).fetchall()
+ for f in rf:
+ file_id_map[f["file_name"]] = f["id"]
+
+ ALLOWED_SECTIONS = {
+ "Asset Management", "Compute", "Identity and Access Management",
+ "Logging and Monitoring", "Networking",
+ "Storage - Block Volumes", "Storage - File Storage Service", "Storage - Object Storage",
+ }
+
+ def norm_section(raw):
+ s = _re.sub(r"\s+", " ", (raw or "").strip())
+ if _re.match(r"^Logging and Monitoring\s*\d*$", s, _re.I): return "Logging and Monitoring"
+ if _re.match(r"^Storage$", s, _re.I): return None
+ if _re.match(r"^Storage\s*-\s*Block Volumes$", s, _re.I): return "Storage - Block Volumes"
+ if _re.match(r"^Storage\s*-\s*File Storage Service$", s, _re.I): return "Storage - File Storage Service"
+ if _re.match(r"^Storage\s*-\s*Object Storage$", s, _re.I): return "Storage - Object Storage"
+ return s or None
+
+ seen = set()
+ filtered = []
+ for r in rows:
+ sec = norm_section(r.get("Section", ""))
+ if not sec or sec not in ALLOWED_SECTIONS: continue
+ key = f"{sec}||{r.get('Recommendation #', '')}||{r.get('Title', '')}".lower()
+ if key in seen: continue
+ seen.add(key)
+ r["Section"] = sec
+ status_raw = (r.get("Compliant", "") or "").strip().lower()
+ r["__compliant"] = status_raw in ("yes", "true", "compliant", "ok")
+ pct_str = (r.get("Compliance Percentage Per Recommendation", "") or "").replace(",", ".")
+ m = _re.search(r"(\d+(\.\d+)?)", pct_str)
+ r["__pct"] = max(0, min(100, float(m.group(1)))) if m else None
+ f_str = (r.get("Findings", "") or "").replace(",", "")
+ fm = _re.search(r"\d+", f_str)
+ r["__findings"] = int(fm.group(0)) if fm else None
+ filtered.append(r)
+
+ # RAG remediations (with per-item timeout)
+ for r in filtered:
+ r["__rag_remediation"] = {"description": "", "remediation": ""}
+ if force_rag:
+ from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeout
+ non_compliant = [r for r in filtered if not r["__compliant"]]
+ log.info(f"Compliance report: searching RAG for {len(non_compliant)} non-compliant items")
+ with ThreadPoolExecutor(max_workers=2) as pool:
+ for r in non_compliant:
+ try:
+ fut = pool.submit(_search_cis_remediation,
+ r.get("Title", ""), r.get("Recommendation #", ""), user_id)
+ r["__rag_remediation"] = fut.result(timeout=30)
+ log.info(f" RAG OK: {r.get('Recommendation #', '?')}")
+ except FuturesTimeout:
+ log.warning(f" RAG timeout: {r.get('Recommendation #', '?')}")
+ except Exception as e:
+ log.warning(f" RAG error {r.get('Recommendation #', '?')}: {e}")
+
+ SECTION_ORDER = [
+ "Identity and Access Management", "Networking", "Compute", "Logging and Monitoring",
+ "Storage - Object Storage", "Storage - Block Volumes", "Storage - File Storage Service",
+ "Asset Management",
+ ]
+ from collections import OrderedDict
+ sections = OrderedDict()
+ for sec in SECTION_ORDER:
+ items = [r for r in filtered if r["Section"] == sec]
+ if items:
+ items.sort(key=lambda x: [int(p) if p.isdigit() else 0 for p in (x.get("Recommendation #", "0") or "0").split(".")])
+ passed = sum(1 for x in items if x["__compliant"])
+ sections[sec] = {"items": items, "total": len(items), "passed": passed, "failed": len(items) - passed}
+
+ html = _build_compliance_html(filtered, tenancy_name, extract_date, sections, rid, file_id_map)
+
+ # Cache to disk
+ cache_path = rdir / "compliance_report.html"
+ cache_path.write_text(html, encoding="utf-8")
+ log.info(f"Compliance report cached: {cache_path} ({len(html)} bytes)")
+ return html
+
+
+@app.get("/api/reports/{rid}/compliance-report")
+async def compliance_report(rid: str, regen: int = Query(0)):
+ """Serve cached LAD A-Team CIS Compliance Report, or return 404 if not generated yet."""
+ rdir = REPORTS / rid
+ cache_path = rdir / "compliance_report.html"
+
+ if regen == 1 and cache_path.exists():
+ cache_path.unlink()
+
+ if cache_path.exists():
+ return FileResponse(cache_path, media_type="text/html")
+
+ raise HTTPException(404, "Compliance report not generated yet")
+
+
+@app.post("/api/reports/{rid}/compliance-report/generate")
+async def generate_compliance_report(rid: str, bg: BackgroundTasks, u=Depends(current_user)):
+ """Start generating the compliance report in background."""
+ with db() as c:
+ report = c.execute("SELECT * FROM reports WHERE id=?", (rid,)).fetchone()
+ if not report: raise HTTPException(404, "Report not found")
+
+ rdir = REPORTS / rid
+ csv_path = rdir / "cis_summary_report.csv"
+ if not csv_path.exists():
+ raise HTTPException(404, "cis_summary_report.csv not found")
+
+ # Delete existing cache to signal "generating"
+ cache_path = rdir / "compliance_report.html"
+ if cache_path.exists():
+ cache_path.unlink()
+
+ has_adb = False
+ with db() as c:
+ adb_cfgs = c.execute("SELECT id FROM adb_vector_configs WHERE user_id=? AND is_active=1", (u["id"],)).fetchall()
+ if adb_cfgs:
+ has_adb = True
+
+ user_id = u["id"]
+ log.info(f"Compliance report generation started for {rid} (has_adb={has_adb})")
+
+ def _bg_generate():
+ try:
+ log.info(f"Compliance report thread running for {rid}")
+ _generate_compliance_report(rid, user_id, force_rag=has_adb)
+ log.info(f"Compliance report generation completed for {rid}")
+ except Exception as e:
+ log.error(f"Compliance report generation failed for {rid}: {e}", exc_info=True)
+
+ _chat_executor.submit(_bg_generate)
+ return {"status": "generating", "has_rag": has_adb}
+
+
+@app.get("/api/reports/{rid}/compliance-report/status")
+async def compliance_report_status(rid: str):
+ """Check if the compliance report exists."""
+ cache_path = REPORTS / rid / "compliance_report.html"
+ return {"ready": cache_path.exists()}
+
@app.get("/api/reports/{rid}/download")
async def report_dl(rid, fmt: str = Query("json"), token: str = Query(None), cred: HTTPAuthorizationCredentials = Depends(HTTPBearer(auto_error=False))):
u = _user_from_token(token) if token else (await current_user(cred) if cred else None)
@@ -4261,11 +4993,11 @@ async def _chat_background(mid: str, sid: str, msg: ChatMsg, user: dict, genai_c
base_prompt = cfg_dict.get("system_prompt", "")
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 + force reasoning_effort=high ββ
+ # ββ Terraform agent: boost max_tokens (capped at 65K to avoid context overflow) + force reasoning_effort=high ββ
if agent_type == "terraform":
tf_model_info = GENAI_MODELS.get(cfg_dict.get("model_id", ""), {})
tf_model_max = tf_model_info.get("max_tokens", 32768)
- cfg_dict["max_tokens"] = tf_model_max
+ cfg_dict["max_tokens"] = min(tf_model_max, 65000)
if tf_model_info.get("reasoning") and not cfg_dict.get("reasoning_effort"):
cfg_dict["reasoning_effort"] = "HIGH"
diff --git a/docker-compose.yml b/docker-compose.yml
index bf60e80..0bda682 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -26,6 +26,7 @@ services:
restart: unless-stopped
volumes:
- ./frontend/index.html:/usr/share/nginx/html/index.html:ro
+ - ./frontend-react/dist:/usr/share/nginx/html/app:ro
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
ports:
- "${PORT:-8080}:80"
diff --git a/frontend-react/README.md b/frontend-react/README.md
new file mode 100644
index 0000000..7dbf7eb
--- /dev/null
+++ b/frontend-react/README.md
@@ -0,0 +1,73 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
+
+## React Compiler
+
+The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
+
+```js
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+
+ // Remove tseslint.configs.recommended and replace with this
+ tseslint.configs.recommendedTypeChecked,
+ // Alternatively, use this for stricter rules
+ tseslint.configs.strictTypeChecked,
+ // Optionally, add this for stylistic rules
+ tseslint.configs.stylisticTypeChecked,
+
+ // Other configs...
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+```
+
+You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
+
+```js
+// eslint.config.js
+import reactX from 'eslint-plugin-react-x'
+import reactDom from 'eslint-plugin-react-dom'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+ // Enable lint rules for React
+ reactX.configs['recommended-typescript'],
+ // Enable lint rules for React DOM
+ reactDom.configs.recommended,
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+```
diff --git a/frontend-react/eslint.config.js b/frontend-react/eslint.config.js
new file mode 100644
index 0000000..5e6b472
--- /dev/null
+++ b/frontend-react/eslint.config.js
@@ -0,0 +1,23 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/frontend-react/index.html b/frontend-react/index.html
new file mode 100644
index 0000000..94290aa
--- /dev/null
+++ b/frontend-react/index.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ AI Agent - Infrastructure & Security Engineer
+
+
+
+
+
+
+
+
diff --git a/frontend-react/package-lock.json b/frontend-react/package-lock.json
new file mode 100644
index 0000000..cbb11ea
--- /dev/null
+++ b/frontend-react/package-lock.json
@@ -0,0 +1,5920 @@
+{
+ "name": "frontend-react",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "frontend-react",
+ "version": "0.0.0",
+ "dependencies": {
+ "axios": "^1.13.6",
+ "lucide-react": "^0.577.0",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4",
+ "react-markdown": "^10.1.0",
+ "react-router-dom": "^7.13.1",
+ "recharts": "^3.8.0",
+ "rehype-highlight": "^7.0.2",
+ "remark-gfm": "^4.0.1",
+ "zustand": "^5.0.11"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.4",
+ "@tailwindcss/postcss": "^4.2.1",
+ "@types/node": "^24.12.0",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.0",
+ "eslint": "^9.39.4",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.4.0",
+ "postcss": "^8.5.8",
+ "tailwindcss": "^4.2.1",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.56.1",
+ "vite": "^8.0.0"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
+ "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
+ "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.0",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
+ "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
+ "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
+ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
+ "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.14.0",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.5",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
+ "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
+ "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1",
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@oxc-project/runtime": {
+ "version": "0.115.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz",
+ "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.115.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz",
+ "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.11.2",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
+ "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@standard-schema/utils": "^0.3.0",
+ "immer": "^11.0.0",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reduxjs/toolkit/node_modules/immer": {
+ "version": "11.1.4",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz",
+ "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz",
+ "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz",
+ "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz",
+ "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz",
+ "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz",
+ "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz",
+ "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz",
+ "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz",
+ "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz",
+ "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz",
+ "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz",
+ "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz",
+ "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz",
+ "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@napi-rs/wasm-runtime": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz",
+ "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz",
+ "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.7",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
+ "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/utils": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+ "license": "MIT"
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz",
+ "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "^5.19.0",
+ "jiti": "^2.6.1",
+ "lightningcss": "1.31.1",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.2.1"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz",
+ "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.31.1",
+ "lightningcss-darwin-arm64": "1.31.1",
+ "lightningcss-darwin-x64": "1.31.1",
+ "lightningcss-freebsd-x64": "1.31.1",
+ "lightningcss-linux-arm-gnueabihf": "1.31.1",
+ "lightningcss-linux-arm64-gnu": "1.31.1",
+ "lightningcss-linux-arm64-musl": "1.31.1",
+ "lightningcss-linux-x64-gnu": "1.31.1",
+ "lightningcss-linux-x64-musl": "1.31.1",
+ "lightningcss-win32-arm64-msvc": "1.31.1",
+ "lightningcss-win32-x64-msvc": "1.31.1"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-android-arm64": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz",
+ "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz",
+ "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-x64": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz",
+ "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-freebsd-x64": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz",
+ "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz",
+ "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz",
+ "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz",
+ "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz",
+ "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz",
+ "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz",
+ "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz",
+ "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz",
+ "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.2.1",
+ "@tailwindcss/oxide-darwin-arm64": "4.2.1",
+ "@tailwindcss/oxide-darwin-x64": "4.2.1",
+ "@tailwindcss/oxide-freebsd-x64": "4.2.1",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.2.1",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.2.1",
+ "@tailwindcss/oxide-linux-x64-musl": "4.2.1",
+ "@tailwindcss/oxide-wasm32-wasi": "4.2.1",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.2.1"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz",
+ "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz",
+ "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz",
+ "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz",
+ "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz",
+ "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz",
+ "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz",
+ "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz",
+ "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz",
+ "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz",
+ "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.8.1",
+ "@emnapi/runtime": "^1.8.1",
+ "@emnapi/wasi-threads": "^1.1.0",
+ "@napi-rs/wasm-runtime": "^1.1.1",
+ "@tybys/wasm-util": "^0.10.1",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz",
+ "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz",
+ "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/postcss": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.1.tgz",
+ "integrity": "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "@tailwindcss/node": "4.2.1",
+ "@tailwindcss/oxide": "4.2.1",
+ "postcss": "^8.5.6",
+ "tailwindcss": "4.2.1"
+ }
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/debug": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/estree-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/mdast": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.12.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
+ "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.14",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
+ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz",
+ "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.57.0",
+ "@typescript-eslint/type-utils": "8.57.0",
+ "@typescript-eslint/utils": "8.57.0",
+ "@typescript-eslint/visitor-keys": "8.57.0",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.57.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz",
+ "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.57.0",
+ "@typescript-eslint/types": "8.57.0",
+ "@typescript-eslint/typescript-estree": "8.57.0",
+ "@typescript-eslint/visitor-keys": "8.57.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz",
+ "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.57.0",
+ "@typescript-eslint/types": "^8.57.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz",
+ "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.57.0",
+ "@typescript-eslint/visitor-keys": "8.57.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz",
+ "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz",
+ "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.57.0",
+ "@typescript-eslint/typescript-estree": "8.57.0",
+ "@typescript-eslint/utils": "8.57.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz",
+ "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz",
+ "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.57.0",
+ "@typescript-eslint/tsconfig-utils": "8.57.0",
+ "@typescript-eslint/types": "8.57.0",
+ "@typescript-eslint/visitor-keys": "8.57.0",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
+ "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "10.2.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
+ "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz",
+ "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.57.0",
+ "@typescript-eslint/types": "8.57.0",
+ "@typescript-eslint/typescript-estree": "8.57.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz",
+ "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.57.0",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "license": "ISC"
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
+ "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "1.0.0-rc.7"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.13.6",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
+ "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.7",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.7.tgz",
+ "integrity": "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001778",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz",
+ "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/ccount": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-reference-invalid": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+ "license": "MIT"
+ },
+ "node_modules/decode-named-character-reference": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
+ "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.313",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz",
+ "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.20.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
+ "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-toolkit": {
+ "version": "1.45.1",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz",
+ "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks"
+ ]
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
+ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.2",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.5",
+ "@eslint/js": "9.39.4",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.5",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
+ "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz",
+ "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": "^9 || ^10"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-util-is-identifier-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+ "license": "MIT"
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz",
+ "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "17.4.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz",
+ "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hast-util-is-element": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz",
+ "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-to-jsx-runtime": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
+ "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-js": "^1.0.0",
+ "unist-util-position": "^5.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-to-text": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz",
+ "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "hast-util-is-element": "^3.0.0",
+ "unist-util-find-after": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/highlight.js": {
+ "version": "11.11.1",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+ "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/html-url-attributes": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
+ "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/immer": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
+ "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inline-style-parser": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
+ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
+ "license": "MIT"
+ },
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/is-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-hexadecimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jiti": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
+ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/lowlight": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
+ "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "devlop": "^1.0.0",
+ "highlight.js": "~11.11.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.577.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz",
+ "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/markdown-table": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
+ "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mdast-util-find-and-replace": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
+ "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "escape-string-regexp": "^5.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mdast-util-from-markdown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
+ "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark": "^4.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
+ "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-gfm-autolink-literal": "^2.0.0",
+ "mdast-util-gfm-footnote": "^2.0.0",
+ "mdast-util-gfm-strikethrough": "^2.0.0",
+ "mdast-util-gfm-table": "^2.0.0",
+ "mdast-util-gfm-task-list-item": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-autolink-literal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
+ "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-find-and-replace": "^3.0.0",
+ "micromark-util-character": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-strikethrough": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
+ "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-table": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
+ "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "markdown-table": "^3.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-task-list-item": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
+ "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-expression": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
+ "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "parse-entities": "^4.0.0",
+ "stringify-entities": "^4.0.0",
+ "unist-util-stringify-position": "^4.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdxjs-esm": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-phrasing": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-markdown": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
+ "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-phrasing": "^4.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "unist-util-visit": "^5.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
+ "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-core-commonmark": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
+ "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-destination": "^2.0.0",
+ "micromark-factory-label": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-factory-title": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-html-tag-name": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-extension-gfm": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
+ "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-extension-gfm-autolink-literal": "^2.0.0",
+ "micromark-extension-gfm-footnote": "^2.0.0",
+ "micromark-extension-gfm-strikethrough": "^2.0.0",
+ "micromark-extension-gfm-table": "^2.0.0",
+ "micromark-extension-gfm-tagfilter": "^2.0.0",
+ "micromark-extension-gfm-task-list-item": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-autolink-literal": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
+ "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-strikethrough": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
+ "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-table": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
+ "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-tagfilter": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
+ "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-task-list-item": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
+ "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-factory-destination": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-label": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-space": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-title": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-whitespace": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-chunked": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-classify-character": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-string": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-resolve-all": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-subtokenize": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.36",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
+ "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-entities": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "character-entities-legacy": "^3.0.0",
+ "character-reference-invalid": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0",
+ "is-hexadecimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-entities/node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/property-information": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
+ "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
+ "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
+ "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.4"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz",
+ "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/react-markdown": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
+ "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "hast-util-to-jsx-runtime": "^2.0.0",
+ "html-url-attributes": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.0.0",
+ "unified": "^11.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18",
+ "react": ">=18"
+ }
+ },
+ "node_modules/react-redux": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
+ "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.13.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz",
+ "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.13.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz",
+ "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.13.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/recharts": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.0.tgz",
+ "integrity": "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==",
+ "license": "MIT",
+ "workspaces": [
+ "www"
+ ],
+ "dependencies": {
+ "@reduxjs/toolkit": "^1.9.0 || 2.x.x",
+ "clsx": "^2.1.1",
+ "decimal.js-light": "^2.5.1",
+ "es-toolkit": "^1.39.3",
+ "eventemitter3": "^5.0.1",
+ "immer": "^10.1.1",
+ "react-redux": "8.x.x || 9.x.x",
+ "reselect": "5.1.1",
+ "tiny-invariant": "^1.3.3",
+ "use-sync-external-store": "^1.2.2",
+ "victory-vendor": "^37.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "license": "MIT"
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
+ "node_modules/rehype-highlight": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz",
+ "integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-to-text": "^4.0.0",
+ "lowlight": "^3.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-gfm": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
+ "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-gfm": "^3.0.0",
+ "micromark-extension-gfm": "^3.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-stringify": "^11.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-parse": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-rehype": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
+ "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "unified": "^11.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-stringify": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
+ "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+ "license": "MIT"
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz",
+ "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.115.0",
+ "@rolldown/pluginutils": "1.0.0-rc.9"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.0-rc.9",
+ "@rolldown/binding-darwin-arm64": "1.0.0-rc.9",
+ "@rolldown/binding-darwin-x64": "1.0.0-rc.9",
+ "@rolldown/binding-freebsd-x64": "1.0.0-rc.9",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9",
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9",
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9",
+ "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9",
+ "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9",
+ "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9",
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9"
+ }
+ },
+ "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz",
+ "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/style-to-js": {
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
+ "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "style-to-object": "1.0.14"
+ }
+ },
+ "node_modules/style-to-object": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
+ "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
+ "license": "MIT",
+ "dependencies": {
+ "inline-style-parser": "0.2.7"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz",
+ "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
+ "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/trough": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
+ "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz",
+ "integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.57.0",
+ "@typescript-eslint/parser": "8.57.0",
+ "@typescript-eslint/typescript-estree": "8.57.0",
+ "@typescript-eslint/utils": "8.57.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unified": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "bail": "^2.0.0",
+ "devlop": "^1.0.0",
+ "extend": "^3.0.0",
+ "is-plain-obj": "^4.0.0",
+ "trough": "^2.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-find-after": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz",
+ "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-is": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
+ "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/vfile": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/victory-vendor": {
+ "version": "37.3.6",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
+ "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
+ "license": "MIT AND ISC",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz",
+ "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/runtime": "0.115.0",
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.8",
+ "rolldown": "1.0.0-rc.9",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.0.0-alpha.31",
+ "esbuild": "^0.27.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "5.0.11",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz",
+ "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ }
+ }
+}
diff --git a/frontend-react/package.json b/frontend-react/package.json
new file mode 100644
index 0000000..d5d9d33
--- /dev/null
+++ b/frontend-react/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "frontend-react",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "axios": "^1.13.6",
+ "lucide-react": "^0.577.0",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4",
+ "react-markdown": "^10.1.0",
+ "react-router-dom": "^7.13.1",
+ "recharts": "^3.8.0",
+ "rehype-highlight": "^7.0.2",
+ "remark-gfm": "^4.0.1",
+ "zustand": "^5.0.11"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.4",
+ "@tailwindcss/postcss": "^4.2.1",
+ "@types/node": "^24.12.0",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.0",
+ "eslint": "^9.39.4",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.4.0",
+ "postcss": "^8.5.8",
+ "tailwindcss": "^4.2.1",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.56.1",
+ "vite": "^8.0.0"
+ }
+}
diff --git a/frontend-react/postcss.config.js b/frontend-react/postcss.config.js
new file mode 100644
index 0000000..a34a3d5
--- /dev/null
+++ b/frontend-react/postcss.config.js
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+};
diff --git a/frontend-react/public/logo.svg b/frontend-react/public/logo.svg
new file mode 100644
index 0000000..0b7b905
--- /dev/null
+++ b/frontend-react/public/logo.svg
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend-react/src/App.tsx b/frontend-react/src/App.tsx
new file mode 100644
index 0000000..9b339f5
--- /dev/null
+++ b/frontend-react/src/App.tsx
@@ -0,0 +1,63 @@
+import { useEffect } from 'react';
+import { Routes, Route, Navigate } from 'react-router-dom';
+import { useAuthStore } from '@/stores/auth';
+import { useAppStore } from '@/stores/app';
+import AppShell from '@/components/layout/AppShell';
+import LoginPage from '@/pages/LoginPage';
+import ChatPage from '@/pages/ChatPage';
+import TerraformPage from '@/pages/TerraformPage';
+import PromptGeneratorPage from '@/pages/PromptGeneratorPage';
+import WorkspacesPage from '@/pages/WorkspacesPage';
+import ExplorerPage from '@/pages/ExplorerPage';
+import ReportsPage from '@/pages/ReportsPage';
+import OciConfigPage from '@/pages/config/OciConfigPage';
+import GenAiConfigPage from '@/pages/config/GenAiConfigPage';
+import McpServersPage from '@/pages/config/McpServersPage';
+import AdbConfigPage from '@/pages/config/AdbConfigPage';
+import EmbeddingsPage from '@/pages/config/EmbeddingsPage';
+import EmbConsultPage from '@/pages/config/EmbConsultPage';
+import UsersPage from '@/pages/admin/UsersPage';
+import MfaPage from '@/pages/admin/MfaPage';
+import AuditPage from '@/pages/admin/AuditPage';
+
+export default function App() {
+ const checkAuth = useAuthStore((s) => s.checkAuth);
+ const loadData = useAppStore((s) => s.loadData);
+ const user = useAuthStore((s) => s.user);
+
+ useEffect(() => {
+ checkAuth().then(() => {
+ if (useAuthStore.getState().user) loadData();
+ });
+ }, [checkAuth, loadData]);
+
+ // Reload data when user changes (login)
+ useEffect(() => {
+ if (user) loadData();
+ }, [user, loadData]);
+
+ return (
+
+ } />
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+ } />
+
+ );
+}
diff --git a/frontend-react/src/api/client.ts b/frontend-react/src/api/client.ts
new file mode 100644
index 0000000..41afac2
--- /dev/null
+++ b/frontend-react/src/api/client.ts
@@ -0,0 +1,30 @@
+import axios from 'axios';
+
+const client = axios.create({
+ baseURL: '/api',
+ headers: { 'Content-Type': 'application/json' },
+});
+
+// Request: attach JWT
+client.interceptors.request.use((config) => {
+ const token = localStorage.getItem('t');
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+});
+
+// Response: handle 401 + extract data
+client.interceptors.response.use(
+ (res) => res.data,
+ (err) => {
+ if (err.response?.status === 401) {
+ localStorage.removeItem('t');
+ window.location.href = '/app/login';
+ }
+ const detail = err.response?.data?.detail || err.response?.data?.message || err.message || 'Erro';
+ return Promise.reject(new Error(detail));
+ }
+);
+
+export default client;
diff --git a/frontend-react/src/api/endpoints/adb.ts b/frontend-react/src/api/endpoints/adb.ts
new file mode 100644
index 0000000..dc654f2
--- /dev/null
+++ b/frontend-react/src/api/endpoints/adb.ts
@@ -0,0 +1,71 @@
+import client from '../client';
+
+export interface AdbTableDef {
+ id: string;
+ table_name: string;
+ description?: string;
+ is_active: boolean;
+}
+
+export interface AdbConfigFull {
+ id: string;
+ config_name: string;
+ dsn: string;
+ username: string;
+ table_name?: string;
+ use_mtls: boolean;
+ is_active: boolean;
+ wallet_dir?: string;
+ genai_config_id?: string;
+ embedding_model_id?: string;
+ tables?: AdbTableDef[];
+ created_at?: string;
+}
+
+export interface AdbTestResult {
+ status: 'success' | 'error';
+ message: string;
+}
+
+export interface WalletParseResult {
+ dsn_names: string[];
+ files: string[];
+}
+
+export const adbApi = {
+ list: () =>
+ client.get('/adb/configs') as unknown as Promise,
+
+ create: (fd: FormData) =>
+ client.post('/adb/config', fd, { headers: { 'Content-Type': 'multipart/form-data' } }) as unknown as Promise<{ id: string; config_name: string }>,
+
+ update: (id: string, fd: FormData) =>
+ client.put(`/adb/configs/${id}`, fd, { headers: { 'Content-Type': 'multipart/form-data' } }) as unknown as Promise<{ id: string; config_name: string }>,
+
+ remove: (id: string) =>
+ client.delete(`/adb/configs/${id}`) as unknown as Promise<{ ok: boolean }>,
+
+ test: (id: string) =>
+ client.post(`/adb/test/${id}`) as unknown as Promise,
+
+ parseWallet: (file: File) => {
+ const fd = new FormData();
+ fd.append('wallet', file);
+ return client.post('/adb/parse-wallet', fd, { headers: { 'Content-Type': 'multipart/form-data' } }) as unknown as Promise;
+ },
+
+ uploadWallet: (id: string, file: File) => {
+ const fd = new FormData();
+ fd.append('wallet', file);
+ return client.post(`/adb/${id}/upload-wallet`, fd, { headers: { 'Content-Type': 'multipart/form-data' } }) as unknown as Promise<{ ok: boolean; files: string[] }>;
+ },
+
+ addTable: (vid: string, tableName: string, description: string) =>
+ client.post(`/adb/${vid}/tables`, { table_name: tableName, description }) as unknown as Promise<{ id: string }>,
+
+ updateTable: (vid: string, tid: string, body: { table_name?: string; description?: string; is_active?: boolean }) =>
+ client.put(`/adb/${vid}/tables/${tid}`, body) as unknown as Promise<{ ok: boolean }>,
+
+ removeTable: (vid: string, tid: string) =>
+ client.delete(`/adb/${vid}/tables/${tid}`) as unknown as Promise<{ ok: boolean }>,
+};
diff --git a/frontend-react/src/api/endpoints/admin.ts b/frontend-react/src/api/endpoints/admin.ts
new file mode 100644
index 0000000..1e2e871
--- /dev/null
+++ b/frontend-react/src/api/endpoints/admin.ts
@@ -0,0 +1,72 @@
+import client from '../client';
+
+/* ββ Types ββ */
+
+export interface AuditEntry {
+ id: string;
+ user_id: string | null;
+ username: string | null;
+ action: string;
+ resource: string | null;
+ details: string | null;
+ ip: string | null;
+ created_at: string;
+}
+
+export interface ConfigLogEntry {
+ id: string;
+ config_type: string;
+ config_id: string;
+ config_name: string | null;
+ severity: string;
+ action: string;
+ message: string;
+ user_id: string | null;
+ username: string | null;
+ created_at: string;
+}
+
+export interface ChatLogEntry {
+ id: string;
+ session_id: string | null;
+ message_id: string | null;
+ user_id: string | null;
+ severity: string;
+ source: string;
+ action: string;
+ message: string;
+ created_at: string;
+}
+
+/* ββ API calls ββ */
+
+export const adminApi = {
+ getAuditLog: (limit = 100) =>
+ client.get(`/audit-log?limit=${limit}`) as unknown as Promise,
+
+ getConfigLogs: (params: {
+ config_type?: string;
+ config_id?: string;
+ severity?: string;
+ limit?: number;
+ } = {}) => {
+ const qs = new URLSearchParams();
+ if (params.config_type) qs.set('config_type', params.config_type);
+ if (params.config_id) qs.set('config_id', params.config_id);
+ if (params.severity) qs.set('severity', params.severity);
+ qs.set('limit', String(params.limit ?? 50));
+ return client.get(`/config-logs?${qs}`) as unknown as Promise;
+ },
+
+ getChatLogs: (params: {
+ severity?: string;
+ session_id?: string;
+ limit?: number;
+ } = {}) => {
+ const qs = new URLSearchParams();
+ if (params.severity) qs.set('severity', params.severity);
+ if (params.session_id) qs.set('session_id', params.session_id);
+ qs.set('limit', String(params.limit ?? 50));
+ return client.get(`/chat-logs?${qs}`) as unknown as Promise;
+ },
+};
diff --git a/frontend-react/src/api/endpoints/auth.ts b/frontend-react/src/api/endpoints/auth.ts
new file mode 100644
index 0000000..4e058fd
--- /dev/null
+++ b/frontend-react/src/api/endpoints/auth.ts
@@ -0,0 +1,31 @@
+import client from '../client';
+
+export interface User {
+ id: string;
+ username: string;
+ role: string;
+ first_name?: string;
+ last_name?: string;
+ email?: string;
+ mfa_enabled?: boolean;
+}
+
+export interface LoginResponse {
+ token?: string;
+ user?: User;
+ mfa_required?: boolean;
+ mfa_setup?: boolean;
+ totp_uri?: string;
+}
+
+export const authApi = {
+ login: (username: string, password: string, totp_code?: string) =>
+ client.post('/auth/login', { username, password, ...(totp_code && { totp_code }) }),
+
+ logout: () => client.post('/auth/logout'),
+
+ me: () => client.get('/users/me'),
+
+ changePassword: (current_password: string, new_password: string) =>
+ client.post('/auth/change-password', { current_password, new_password }),
+};
diff --git a/frontend-react/src/api/endpoints/chat.ts b/frontend-react/src/api/endpoints/chat.ts
new file mode 100644
index 0000000..97fc77f
--- /dev/null
+++ b/frontend-react/src/api/endpoints/chat.ts
@@ -0,0 +1,94 @@
+import client from '../client';
+
+/* ββ Types ββ */
+
+export interface ChatSession {
+ id: string;
+ title: string;
+ agent_type: string;
+ created_at: string;
+ updated_at: string;
+ message_count?: number;
+}
+
+export interface ChatMessage {
+ id: string;
+ session_id?: string;
+ role: 'user' | 'assistant';
+ content: string;
+ model_id?: string;
+ status?: 'processing' | 'done' | 'failed';
+ created_at?: string;
+}
+
+export interface ChatSendBody {
+ message: string;
+ session_id?: string;
+ genai_config_id?: string;
+ model_id?: string;
+ oci_config_id?: string;
+ genai_region?: string;
+ compartment_id?: string;
+ temperature?: number;
+ max_tokens?: number;
+ top_p?: number;
+ top_k?: number;
+ frequency_penalty?: number;
+ presence_penalty?: number;
+ reasoning_effort?: string;
+ use_tools?: boolean;
+}
+
+export interface ChatSendResponse {
+ message_id: string;
+ session_id: string;
+ status: 'processing' | 'done';
+}
+
+export interface MessageStatusResponse {
+ id: string;
+ session_id: string;
+ role: string;
+ content: string;
+ model_id?: string;
+ status: 'processing' | 'done' | 'failed';
+}
+
+export interface SessionMessagesResponse {
+ session: ChatSession;
+ messages: ChatMessage[];
+}
+
+/* ββ API ββ */
+
+export const chatApi = {
+ /** Send a chat message (text only) */
+ send: (body: ChatSendBody) =>
+ client.post('/chat', body) as unknown as Promise,
+
+ /** Send a chat message with file attachments */
+ upload: (formData: FormData) =>
+ client.post('/chat/upload', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ }) as unknown as Promise,
+
+ /** Poll message processing status */
+ pollStatus: (messageId: string) =>
+ client.get(`/chat/${messageId}/status`) as unknown as Promise,
+
+ /** List chat sessions */
+ listSessions: (agentType = 'chat', limit = 50) =>
+ client.get(`/chat/sessions?agent_type=${agentType}&limit=${limit}`) as unknown as Promise,
+
+ /** Load session with all messages */
+ loadSession: (sessionId: string) =>
+ client.get(`/chat/sessions/${sessionId}/messages`) as unknown as Promise,
+
+ /** Delete a session */
+ deleteSession: (sessionId: string) =>
+ client.delete(`/chat/${sessionId}`) as unknown as Promise<{ ok: boolean }>,
+
+ /** Rename a session */
+ renameSession: (sessionId: string, title: string) =>
+ client.put(`/chat/sessions/${sessionId}/title`, { title }) as unknown as Promise<{ ok: boolean }>,
+};
diff --git a/frontend-react/src/api/endpoints/embeddings.ts b/frontend-react/src/api/endpoints/embeddings.ts
new file mode 100644
index 0000000..1f51d75
--- /dev/null
+++ b/frontend-react/src/api/endpoints/embeddings.ts
@@ -0,0 +1,97 @@
+import client from '../client';
+
+/* ββ Types ββ */
+
+export interface EmbeddingDoc {
+ id: string;
+ metadata: string;
+}
+
+export interface EmbeddingListResult {
+ total: number;
+ offset: number;
+ limit: number;
+ documents: EmbeddingDoc[];
+}
+
+export interface EmbedUploadResult {
+ ok: boolean;
+ message: string;
+ chunks: number;
+ filename?: string;
+ url?: string;
+}
+
+export interface PurgeResult {
+ ok: boolean;
+ deleted: number;
+ table: string;
+ tenancy: string;
+}
+
+export interface ConsultResult {
+ answer: string;
+ documents: {
+ content: string;
+ source: string;
+ distance: number;
+ metadata: string;
+ }[];
+ total: number;
+}
+
+/* ββ API ββ */
+
+export const embeddingsApi = {
+ /** List embeddings for a given ADB config + optional table */
+ list: (vid: string, tableName?: string, limit = 50, offset = 0) => {
+ const params = new URLSearchParams();
+ if (tableName) params.set('table_name', tableName);
+ params.set('limit', String(limit));
+ params.set('offset', String(offset));
+ return client.get(`/embeddings/${vid}/list?${params}`) as unknown as Promise;
+ },
+
+ /** Delete a single embedding document */
+ remove: (vid: string, docId: string, tableName?: string) => {
+ const qs = tableName ? `?table_name=${encodeURIComponent(tableName)}` : '';
+ return client.delete(`/embeddings/${vid}/${docId}${qs}`) as unknown as Promise<{ ok: boolean }>;
+ },
+
+ /** Upload a file to generate embeddings */
+ uploadFile: (adbConfigId: string, tableName: string, file: File) => {
+ const fd = new FormData();
+ fd.append('adb_config_id', adbConfigId);
+ fd.append('table_name', tableName);
+ fd.append('file', file);
+ return client.post('/embeddings/upload', fd, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ }) as unknown as Promise;
+ },
+
+ /** Import a URL to generate embeddings */
+ uploadUrl: (adbConfigId: string, tableName: string, url: string) => {
+ const fd = new FormData();
+ fd.append('adb_config_id', adbConfigId);
+ fd.append('table_name', tableName);
+ fd.append('url', url);
+ return client.post('/embeddings/upload-url', fd, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ }) as unknown as Promise;
+ },
+
+ /** Purge embeddings from a table (optionally filtered by tenancy) */
+ purge: (vid: string, tableName: string, tenancy?: string) =>
+ client.post(`/embeddings/${vid}/purge`, {
+ table_name: tableName,
+ tenancy: tenancy || '',
+ }) as unknown as Promise,
+
+ /** Consult embeddings with a question */
+ consult: (query: string, tableName?: string, topK = 10) =>
+ client.post('/embeddings/consult', {
+ query,
+ table_name: tableName || '',
+ top_k: topK,
+ }) as unknown as Promise,
+};
diff --git a/frontend-react/src/api/endpoints/explorer.ts b/frontend-react/src/api/endpoints/explorer.ts
new file mode 100644
index 0000000..5247ce2
--- /dev/null
+++ b/frontend-react/src/api/endpoints/explorer.ts
@@ -0,0 +1,129 @@
+import client from '../client';
+
+/* ββ Types βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+export interface CompartmentNode {
+ id: string;
+ name: string;
+ parent_id: string | null;
+ lifecycle_state?: string;
+}
+
+export interface Region {
+ name: string;
+ key: string;
+ status: string;
+ is_home: boolean;
+}
+
+export interface CountEntry {
+ count: number;
+ updated_at: string;
+}
+
+export type CountsMap = Record;
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export type ResourceItem = Record;
+
+export interface ActionResult {
+ ok: boolean;
+ action: string;
+ [key: string]: unknown;
+}
+
+export interface MyIpResult {
+ ip: string;
+}
+
+/* ββ API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+export const explorerApi = {
+ /* Compartment tree (flat list with parent_id) */
+ getCompartmentTree: (cid: string) =>
+ client.get(`/oci/explore/${cid}/compartment-tree`) as unknown as Promise,
+
+ /* Regions subscribed */
+ getRegions: (cid: string) =>
+ client.get(`/oci/explore/${cid}/regions`) as unknown as Promise,
+
+ /* Cached counts */
+ getCounts: (cid: string, compartmentId: string, regions: string = '') =>
+ client.get(`/oci/explore/${cid}/counts`, {
+ params: { compartment_id: compartmentId, regions },
+ }) as unknown as Promise,
+
+ /* Trigger background counts refresh */
+ refreshCounts: (cid: string, compartmentId: string, regions: string[]) =>
+ client.post(`/oci/explore/${cid}/counts/refresh`, {
+ compartment_id: compartmentId,
+ regions,
+ }) as unknown as Promise<{ status: string; types: number }>,
+
+ /* Fetch resources of a given type */
+ getResources: (cid: string, resourceType: string, compartmentId: string, region?: string) =>
+ client.get(`/oci/explore/${cid}/${resourceType}`, {
+ params: { compartment_id: compartmentId, region: region || undefined },
+ }) as unknown as Promise,
+
+ /* ββ Actions βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+ instanceAction: (id: string, action: 'START' | 'STOP', ociConfigId: string, region?: string) =>
+ client.post(`/oci/instances/${id}/action`, {
+ action, oci_config_id: ociConfigId, region,
+ }) as unknown as Promise,
+
+ adbAction: (id: string, action: 'start' | 'stop', ociConfigId: string, region?: string) =>
+ client.post(`/oci/autonomous-databases/${id}/action`, {
+ action, oci_config_id: ociConfigId, region,
+ }) as unknown as Promise,
+
+ dbSystemAction: (id: string, action: 'START' | 'STOP', ociConfigId: string, region?: string) =>
+ client.post(`/oci/db-systems/${id}/action`, {
+ action, oci_config_id: ociConfigId, region,
+ }) as unknown as Promise,
+
+ mysqlAction: (id: string, action: 'start' | 'stop', ociConfigId: string, region?: string) =>
+ client.post(`/oci/mysql-db-systems/${id}/action`, {
+ action, oci_config_id: ociConfigId, region,
+ }) as unknown as Promise,
+
+ containerAction: (id: string, action: 'start' | 'stop', ociConfigId: string, region?: string) =>
+ client.post(`/oci/container-instances/${id}/action`, {
+ action, oci_config_id: ociConfigId, region,
+ }) as unknown as Promise,
+
+ getMyIp: () =>
+ client.get('/oci/my-ip') as unknown as Promise,
+
+ adbUpdateNetwork: (id: string, ip: string, ociConfigId: string, region?: string) =>
+ client.post(`/oci/autonomous-databases/${id}/update-network-access`, {
+ ip, oci_config_id: ociConfigId, region,
+ }) as unknown as Promise,
+
+ /* ββ Security List Rules βββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+ addSlRule: (cid: string, slId: string, body: {
+ direction: string; protocol: string; source_dest: string;
+ port_min?: number; port_max?: number; description?: string;
+ is_stateless?: boolean; region?: string;
+ }) =>
+ client.post(`/oci/explore/${cid}/security_lists/${slId}/rules`, body) as unknown as Promise,
+
+ removeSlRule: (cid: string, slId: string, body: {
+ direction: string; rule_index: number; region?: string;
+ }) =>
+ client.delete(`/oci/explore/${cid}/security_lists/${slId}/rules`, { data: body }) as unknown as Promise,
+
+ /* ββ NSG Rules βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+ addNsgRule: (cid: string, nsgId: string, body: {
+ direction: string; protocol: string; source_dest: string;
+ source_dest_type?: string; port_min?: number; port_max?: number;
+ description?: string; is_stateless?: boolean; region?: string;
+ }) =>
+ client.post(`/oci/explore/${cid}/nsgs/${nsgId}/rules`, body) as unknown as Promise,
+
+ removeNsgRule: (cid: string, nsgId: string, ruleId: string, region?: string) =>
+ client.delete(`/oci/explore/${cid}/nsgs/${nsgId}/rules/${ruleId}${region ? `?region=${region}` : ''}`) as unknown as Promise,
+};
diff --git a/frontend-react/src/api/endpoints/genai.ts b/frontend-react/src/api/endpoints/genai.ts
new file mode 100644
index 0000000..a2fe0f2
--- /dev/null
+++ b/frontend-react/src/api/endpoints/genai.ts
@@ -0,0 +1,66 @@
+import client from '../client';
+
+export interface GenAiConfigFull {
+ id: string;
+ name: string;
+ oci_config_id: string;
+ model_id: string;
+ model_ocid?: string;
+ compartment_id: string;
+ genai_region: string;
+ endpoint?: string;
+ serving_type: string;
+ dedicated_endpoint_id?: string;
+ temperature: number;
+ max_tokens: number;
+ top_p: number;
+ top_k: number;
+ frequency_penalty: number;
+ presence_penalty: number;
+ reasoning_effort?: string;
+ is_default: boolean;
+ created_at?: string;
+}
+
+export interface GenAiConfigPayload {
+ name: string;
+ oci_config_id: string;
+ model_id: string;
+ model_ocid?: string | null;
+ compartment_id: string;
+ genai_region: string;
+ endpoint?: string | null;
+ serving_type?: string;
+ dedicated_endpoint_id?: string | null;
+ temperature?: number;
+ max_tokens?: number;
+ top_p?: number;
+ top_k?: number;
+ frequency_penalty?: number;
+ presence_penalty?: number;
+ reasoning_effort?: string | null;
+ is_default?: boolean;
+}
+
+export interface GenAiTestResult {
+ status: 'success' | 'error';
+ message: string;
+ response?: string;
+}
+
+export const genaiApi = {
+ list: () =>
+ client.get('/genai/configs') as unknown as Promise,
+
+ create: (body: GenAiConfigPayload) =>
+ client.post('/genai/config', body) as unknown as Promise<{ id: string; model_id: string; endpoint: string }>,
+
+ update: (id: string, body: GenAiConfigPayload) =>
+ client.put(`/genai/configs/${id}`, body) as unknown as Promise<{ id: string; model_id: string; endpoint: string }>,
+
+ remove: (id: string) =>
+ client.delete(`/genai/configs/${id}`) as unknown as Promise<{ ok: boolean }>,
+
+ test: (id: string) =>
+ client.post(`/genai/test/${id}`) as unknown as Promise,
+};
diff --git a/frontend-react/src/api/endpoints/mcp.ts b/frontend-react/src/api/endpoints/mcp.ts
new file mode 100644
index 0000000..899ac4d
--- /dev/null
+++ b/frontend-react/src/api/endpoints/mcp.ts
@@ -0,0 +1,68 @@
+import client from '../client';
+
+export interface McpServerFull {
+ id: string;
+ name: string;
+ description?: string;
+ server_type: string;
+ command?: string;
+ args?: string[];
+ env_vars?: Record;
+ url?: string;
+ module_path?: string;
+ is_active: boolean;
+ tools?: McpToolDef[];
+ linked_adb_id?: string;
+ created_at?: string;
+}
+
+export interface McpToolDef {
+ name: string;
+ description?: string;
+ input_schema?: Record;
+}
+
+export interface McpServerPayload {
+ name: string;
+ description?: string | null;
+ server_type: string;
+ command?: string | null;
+ args?: string[] | null;
+ env_vars?: Record | null;
+ url?: string | null;
+ module_path?: string | null;
+ tools?: McpToolDef[] | null;
+ linked_adb_id?: string | null;
+}
+
+export const mcpApi = {
+ list: () =>
+ client.get('/mcp/servers') as unknown as Promise,
+
+ create: (body: McpServerPayload) =>
+ client.post('/mcp/servers', body) as unknown as Promise<{ id: string; name: string; server_type: string }>,
+
+ update: (id: string, body: McpServerPayload) =>
+ client.put(`/mcp/servers/${id}`, body) as unknown as Promise<{ id: string; name: string; server_type: string }>,
+
+ remove: (id: string) =>
+ client.delete(`/mcp/servers/${id}`) as unknown as Promise<{ ok: boolean }>,
+
+ toggle: (id: string) =>
+ client.put(`/mcp/servers/${id}/toggle`) as unknown as Promise<{ ok: boolean; is_active: boolean }>,
+
+ discoverTools: (id: string) =>
+ client.post(`/mcp/servers/${id}/discover-tools`) as unknown as Promise<{ ok: boolean; discovered: number; total: number; tools: McpToolDef[] }>,
+
+ updateTools: (id: string, tools: McpToolDef[]) =>
+ client.put(`/mcp/servers/${id}/tools`, { tools }) as unknown as Promise<{ ok: boolean }>,
+
+ upload: (id: string, file: File) => {
+ const fd = new FormData();
+ fd.append('file', file);
+ return client.post(`/mcp/servers/${id}/upload`, fd, { headers: { 'Content-Type': 'multipart/form-data' } }) as unknown as Promise<{ ok: boolean; path: string }>;
+ },
+
+ linkAdb: (id: string, adbId: string) =>
+ client.put(`/mcp/servers/${id}/link-adb?adb_id=${adbId}`) as unknown as Promise<{ ok: boolean }>,
+};
diff --git a/frontend-react/src/api/endpoints/oci.ts b/frontend-react/src/api/endpoints/oci.ts
new file mode 100644
index 0000000..6fe4679
--- /dev/null
+++ b/frontend-react/src/api/endpoints/oci.ts
@@ -0,0 +1,39 @@
+import client from '../client';
+
+export interface OciConfigFull {
+ id: string;
+ user_id: string;
+ tenancy_name: string;
+ tenancy_ocid: string;
+ user_ocid: string;
+ fingerprint: string;
+ region: string;
+ compartment_id?: string;
+ ssh_public_key?: string;
+ ssh_public_key_preview?: string;
+ auth_type: 'api_key' | 'session_token';
+ created_at: string;
+}
+
+export interface OciTestResult {
+ status: 'success' | 'error';
+ message: string;
+ data?: unknown;
+}
+
+export const ociApi = {
+ list: () =>
+ client.get('/oci/configs') as unknown as Promise,
+
+ create: (fd: FormData) =>
+ client.post('/oci/config', fd, { headers: { 'Content-Type': 'multipart/form-data' } }) as unknown as Promise<{ id: string; tenancy_name: string; region: string }>,
+
+ update: (id: string, fd: FormData) =>
+ client.put(`/oci/configs/${id}`, fd, { headers: { 'Content-Type': 'multipart/form-data' } }) as unknown as Promise<{ id: string; tenancy_name: string; region: string }>,
+
+ remove: (id: string) =>
+ client.delete(`/oci/configs/${id}`) as unknown as Promise<{ ok: boolean }>,
+
+ test: (id: string) =>
+ client.post(`/oci/test/${id}`) as unknown as Promise,
+};
diff --git a/frontend-react/src/api/endpoints/reports.ts b/frontend-react/src/api/endpoints/reports.ts
new file mode 100644
index 0000000..b89f8a2
--- /dev/null
+++ b/frontend-react/src/api/endpoints/reports.ts
@@ -0,0 +1,142 @@
+import client from '../client';
+
+/* ββ Types ββ */
+
+export interface Report {
+ id: string;
+ tenancy_name: string;
+ status: string;
+ level: number | null;
+ regions?: string[];
+ obp?: boolean;
+ raw?: boolean;
+ redact_output?: boolean;
+ created_at: string;
+ completed_at: string | null;
+ error_msg?: string | null;
+}
+
+export interface RunReportBody {
+ config_id: string;
+ regions?: string[];
+ level: 1 | 2;
+ obp: boolean;
+ raw: boolean;
+ redact_output: boolean;
+}
+
+export interface RunReportResult {
+ report_id: string;
+ status: string;
+}
+
+export interface ReportProgress {
+ status: string;
+ progress: string[];
+ created_at: string;
+ completed_at: string | null;
+ error_msg: string | null;
+}
+
+export interface ReportSummary {
+ tenancy: string;
+ level: number;
+ regions: string[];
+ generated_at: string;
+ total: number;
+ passed: number;
+ failed: number;
+ total_findings: number;
+ total_compliant: number;
+ score: number;
+ sections: Record;
+}
+
+export interface ReportFile {
+ id: string;
+ file_name: string;
+ file_type: string;
+ file_category: string;
+ file_size: number;
+}
+
+export interface CisEngineVersion {
+ version: string;
+ updated_date: string;
+ file_path: string;
+}
+
+export interface CisEngineUpdate {
+ local_version: string;
+ local_date: string;
+ remote_version: string;
+ remote_date: string;
+ update_available: boolean;
+}
+
+/* ββ API ββ */
+
+export const reportsApi = {
+ run: (body: RunReportBody) =>
+ client.post('/reports/run', body) as unknown as Promise,
+
+ list: () =>
+ client.get('/reports') as unknown as Promise,
+
+ progress: (rid: string) =>
+ client.get(`/reports/${rid}/progress`) as unknown as Promise,
+
+ cancel: (rid: string) =>
+ client.post(`/reports/${rid}/cancel`) as unknown as Promise<{ ok: boolean }>,
+
+ delete: (rid: string) =>
+ client.delete(`/reports/${rid}`) as unknown as Promise<{ ok: boolean }>,
+
+ summary: (rid: string) =>
+ client.get(`/reports/${rid}/summary`) as unknown as Promise,
+
+ htmlUrl: (rid: string) => {
+ const token = localStorage.getItem('t') || '';
+ return `/api/reports/${rid}/html?token=${encodeURIComponent(token)}`;
+ },
+
+ complianceReportUrl: (rid: string) => {
+ const token = localStorage.getItem('t') || '';
+ return `/api/reports/${rid}/compliance-report?token=${encodeURIComponent(token)}`;
+ },
+
+ generateComplianceReport: (rid: string) =>
+ client.post(`/reports/${rid}/compliance-report/generate`) as unknown as Promise<{ status: string; has_rag: boolean }>,
+
+ complianceReportStatus: (rid: string) =>
+ client.get(`/reports/${rid}/compliance-report/status`) as unknown as Promise<{ ready: boolean }>,
+
+ files: (rid: string) =>
+ client.get(`/reports/${rid}/files`) as unknown as Promise,
+
+ downloadUrl: (rid: string, fid: string) => {
+ const token = localStorage.getItem('t') || '';
+ return `/api/reports/${rid}/files/${fid}/download?token=${encodeURIComponent(token)}`;
+ },
+
+ engineVersion: () =>
+ client.get('/cis-engine/version') as unknown as Promise,
+
+ engineCheckUpdate: () =>
+ client.get('/cis-engine/check-update') as unknown as Promise,
+
+ engineUpdate: () =>
+ client.post('/cis-engine/update') as unknown as Promise<{ ok: boolean }>,
+
+ embedReport: (rid: string, adbConfigId: string, tableName?: string) =>
+ client.post(`/embeddings/report/${rid}`, {
+ adb_config_id: adbConfigId,
+ ...(tableName ? { table_name: tableName } : {}),
+ }) as unknown as Promise<{ ok: boolean; message: string; sections: number; tenancy: string }>,
+
+ embedFile: (rid: string, fid: string, adbConfigId: string, tableName?: string) =>
+ client.post(`/embeddings/report/${rid}/file/${fid}`, {
+ adb_config_id: adbConfigId,
+ ...(tableName ? { table_name: tableName } : {}),
+ }) as unknown as Promise<{ ok: boolean; message: string; chunks: number }>,
+};
diff --git a/frontend-react/src/api/endpoints/terraform.ts b/frontend-react/src/api/endpoints/terraform.ts
new file mode 100644
index 0000000..8e84f7b
--- /dev/null
+++ b/frontend-react/src/api/endpoints/terraform.ts
@@ -0,0 +1,136 @@
+import client from '../client';
+
+/* ββ Types ββ */
+
+export type WsStatus =
+ | 'draft' | 'planning' | 'planned'
+ | 'applying' | 'applied'
+ | 'destroying' | 'destroyed'
+ | 'failed';
+
+export interface Workspace {
+ id: string;
+ name: string;
+ session_id: string;
+ session_title?: string;
+ oci_config_id: string;
+ compartment_id?: string;
+ status: WsStatus;
+ plan_output?: string;
+ apply_output?: string;
+ destroy_output?: string;
+ error?: string;
+ tf_code?: string;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface WsStatusResult {
+ status: WsStatus;
+ plan_output?: string;
+ apply_output?: string;
+ destroy_output?: string;
+ error?: string;
+ updated_at: string;
+}
+
+export interface ChatSession {
+ id: string;
+ title: string;
+ agent_type: string;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface ChatMessage {
+ id: string;
+ role: 'user' | 'assistant';
+ content: string;
+ created_at?: string;
+}
+
+export interface PromptResponse {
+ message_id: string;
+ session_id: string;
+ status: 'processing' | 'done';
+ prompt?: string;
+}
+
+export interface MessageStatus {
+ status: 'processing' | 'done' | 'failed';
+ content: string;
+}
+
+/* ββ Workspace API ββ */
+
+export const terraformApi = {
+ /* Workspaces */
+ listWorkspaces: () =>
+ client.get('/terraform/workspaces') as unknown as Promise,
+
+ getWorkspace: (wid: string) =>
+ client.get(`/terraform/workspaces/${wid}`) as unknown as Promise,
+
+ deleteWorkspace: (wid: string) =>
+ client.delete(`/terraform/workspaces/${wid}`) as unknown as Promise<{ ok: boolean }>,
+
+ plan: (wid: string) =>
+ client.post(`/terraform/workspaces/${wid}/plan`) as unknown as Promise<{ status: string }>,
+
+ apply: (wid: string) =>
+ client.post(`/terraform/workspaces/${wid}/apply`) as unknown as Promise<{ status: string }>,
+
+ destroy: (wid: string) =>
+ client.post(`/terraform/workspaces/${wid}/destroy`, { confirm: 'DESTROY' }) as unknown as Promise<{ status: string }>,
+
+ cancel: (wid: string) =>
+ client.post(`/terraform/workspaces/${wid}/cancel`) as unknown as Promise<{ ok: boolean }>,
+
+ status: (wid: string) =>
+ client.get(`/terraform/workspaces/${wid}/status`) as unknown as Promise,
+
+ downloadUrl: (wid: string) => {
+ const token = localStorage.getItem('t') || '';
+ return `/api/terraform/workspaces/${wid}/download?token=${encodeURIComponent(token)}`;
+ },
+
+ /* Prompt Generator */
+ generatePrompt: (body: {
+ message: string;
+ session_id?: string;
+ genai_config: Record;
+ history?: { role: string; content: string }[] | null;
+ }) =>
+ client.post('/terraform/generate-prompt', body) as unknown as Promise,
+
+ /* Chat sessions / history */
+ listSessions: (agentType: string, limit = 50) =>
+ client.get(`/chat/sessions?agent_type=${agentType}&limit=${limit}`) as unknown as Promise,
+
+ loadSession: (sid: string) =>
+ client.get(`/chat/sessions/${sid}/messages`) as unknown as Promise<{ messages: ChatMessage[] }>,
+
+ deleteSession: (sid: string) =>
+ client.delete(`/chat/sessions/${sid}`) as unknown as Promise<{ ok: boolean }>,
+
+ pollMessageStatus: (mid: string) =>
+ client.get(`/chat/${mid}/status`) as unknown as Promise,
+
+ createWorkspace: (body: { session_id?: string; oci_config_id: string; compartment_id?: string; name: string; tf_code: string }) =>
+ client.post('/terraform/workspaces', body) as unknown as Promise,
+
+ updateCode: (wid: string, tf_code: string) =>
+ client.put(`/terraform/workspaces/${wid}/code`, { tf_code }) as unknown as Promise<{ ok: boolean }>,
+
+ chat: (body: { message: string; session_id?: string; genai_config_id?: string; model_id?: string; oci_config_id?: string; genai_region?: string; compartment_id?: string; temperature?: number; max_tokens?: number; use_tools?: boolean }) =>
+ client.post('/terraform/chat', body) as unknown as Promise<{ message_id: string; session_id: string; status: string }>,
+
+ loadResources: (ociConfigId: string, compartmentId: string, region?: string) => {
+ const params = new URLSearchParams({ oci_config_id: ociConfigId, compartment_id: compartmentId });
+ if (region) params.set('region', region);
+ return client.get(`/terraform/resources?${params}`) as unknown as Promise>;
+ },
+
+ getCompartmentTree: (cid: string) =>
+ client.get(`/oci/explore/${cid}/compartment-tree`) as unknown as Promise,
+};
diff --git a/frontend-react/src/components/layout/AppShell.tsx b/frontend-react/src/components/layout/AppShell.tsx
new file mode 100644
index 0000000..dd3bccb
--- /dev/null
+++ b/frontend-react/src/components/layout/AppShell.tsx
@@ -0,0 +1,27 @@
+import { Outlet, Navigate } from 'react-router-dom';
+import { useAuthStore } from '@/stores/auth';
+import Sidebar from './Sidebar';
+
+export default function AppShell() {
+ const { user, loading } = useAuthStore();
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (!user) return ;
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/frontend-react/src/components/layout/Sidebar.tsx b/frontend-react/src/components/layout/Sidebar.tsx
new file mode 100644
index 0000000..9379f84
--- /dev/null
+++ b/frontend-react/src/components/layout/Sidebar.tsx
@@ -0,0 +1,186 @@
+import { NavLink } from 'react-router-dom';
+import {
+ MessageSquare, Search, BarChart3, Cloud,
+ Brain, Plug, Database, Dna, Users, Lock, FileText,
+ Sparkles, Sun, Moon, LogOut, FolderOpen, Languages
+} from 'lucide-react';
+import { useTheme } from '@/hooks/useTheme';
+import { useAuthStore } from '@/stores/auth';
+import { useI18n } from '@/i18n';
+
+const LOGO = (
+
+
+
+
+
+
+
+
+
+
+
+);
+
+const SZ = 18;
+
+const TerraformIcon = ({ size = 18 }: { size?: number }) => (
+
+
+
+
+
+
+);
+
+interface NavItem {
+ to: string;
+ icon: React.ReactNode;
+ labelKey: string;
+ adminOnly?: boolean;
+ sub?: boolean;
+}
+
+const NAV_SECTIONS: { titleKey: string; items: NavItem[] }[] = [
+ {
+ titleKey: 'nav.principal',
+ items: [
+ { to: '/chat', icon: , labelKey: 'nav.chat' },
+ { to: '/terraform', icon: , labelKey: 'nav.terraform' },
+ { to: '/terraform/prompt', icon: , labelKey: 'nav.promptGen', sub: true },
+ { to: '/terraform/workspaces', icon: , labelKey: 'nav.workspaces', sub: true },
+ { to: '/explorer', icon: , labelKey: 'nav.explorer' },
+ { to: '/reports', icon: , labelKey: 'nav.reports' },
+ ],
+ },
+ {
+ titleKey: 'nav.config',
+ items: [
+ { to: '/config/oci', icon: , labelKey: 'nav.ociCreds' },
+ { to: '/config/genai', icon: , labelKey: 'nav.genai' },
+ { to: '/config/mcp', icon: , labelKey: 'nav.mcp' },
+ { to: '/config/adb', icon: , labelKey: 'nav.adb' },
+ { to: '/config/embeddings', icon: , labelKey: 'nav.embeddings' },
+ { to: '/config/embeddings/consult', icon: , labelKey: 'nav.embConsult', sub: true },
+ ],
+ },
+ {
+ titleKey: 'nav.admin',
+ items: [
+ { to: '/admin/users', icon: , labelKey: 'nav.users', adminOnly: true },
+ { to: '/admin/mfa', icon: , labelKey: 'nav.mfa' },
+ { to: '/admin/audit', icon: , labelKey: 'nav.audit', adminOnly: true },
+ ],
+ },
+];
+
+export default function Sidebar() {
+ const { toggle, isDark } = useTheme();
+ const { user, logout } = useAuthStore();
+ const { t, lang, setLang } = useI18n();
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ {LOGO}
+
+
AI Agent
+
+ {t('sidebar.subtitle')}
+
+
+
+
+
+ {/* Navigation */}
+
+ {NAV_SECTIONS.map((section) => (
+
+
+ {t(section.titleKey)}
+
+ {section.items
+ .filter((item) => !item.adminOnly || user?.role === 'admin')
+ .map((item) => (
+
isActive ? {
+ background: 'var(--acl2)',
+ color: 'var(--ac)',
+ boxShadow: 'inset 3px 0 0 var(--ac)',
+ } : {
+ color: 'var(--t2)',
+ }}
+ >
+ {item.icon}
+ {t(item.labelKey)}
+
+ ))}
+
+ ))}
+
+
+ {/* Footer */}
+
+
{ e.currentTarget.style.background = 'var(--bg3)'; e.currentTarget.style.color = 'var(--t1)'; }}
+ onMouseLeave={(e) => { e.currentTarget.style.background = ''; e.currentTarget.style.color = 'var(--t3)'; }}
+ >
+ {isDark ? : }
+
+
setLang(lang === 'pt' ? 'en' : 'pt')}
+ className="p-2.5 rounded-xl transition-all duration-200 flex items-center gap-1"
+ style={{ color: 'var(--t3)' }}
+ title={lang === 'pt' ? 'English' : 'PortuguΓͺs'}
+ onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--bg3)'; e.currentTarget.style.color = 'var(--t1)'; }}
+ onMouseLeave={(e) => { e.currentTarget.style.background = ''; e.currentTarget.style.color = 'var(--t3)'; }}
+ >
+
+ {lang}
+
+
+
+ {user?.username?.charAt(0).toUpperCase()}
+
+
+
{user?.username}
+
+ {user?.role === 'admin' ? t('sidebar.roleAdmin') : t('sidebar.roleUser')}
+
+
+
+
{ e.currentTarget.style.background = 'var(--rdl)'; e.currentTarget.style.color = 'var(--rd)'; }}
+ onMouseLeave={(e) => { e.currentTarget.style.background = ''; e.currentTarget.style.color = 'var(--t3)'; }}
+ >
+
+
+
+
+ );
+}
diff --git a/frontend-react/src/components/shared/StubPage.tsx b/frontend-react/src/components/shared/StubPage.tsx
new file mode 100644
index 0000000..9f38607
--- /dev/null
+++ b/frontend-react/src/components/shared/StubPage.tsx
@@ -0,0 +1,68 @@
+import type { ReactNode } from 'react';
+import { ArrowRight, Wrench } from 'lucide-react';
+
+interface StubPageProps {
+ icon: ReactNode;
+ title: string;
+ description: string;
+ legacyTab: string;
+ color?: string;
+ features?: string[];
+}
+
+export default function StubPage({ icon, title, description, legacyTab, color = 'var(--ac)', features }: StubPageProps) {
+ return (
+
+ );
+}
diff --git a/frontend-react/src/hooks/usePolling.ts b/frontend-react/src/hooks/usePolling.ts
new file mode 100644
index 0000000..029840f
--- /dev/null
+++ b/frontend-react/src/hooks/usePolling.ts
@@ -0,0 +1,25 @@
+import { useEffect, useRef } from 'react';
+
+export function usePolling(
+ callback: () => Promise,
+ intervalMs: number,
+ enabled: boolean
+) {
+ const savedCallback = useRef(callback);
+ savedCallback.current = callback;
+
+ useEffect(() => {
+ if (!enabled) return;
+
+ let active = true;
+ const poll = async () => {
+ if (!active) return;
+ const shouldStop = await savedCallback.current();
+ if (shouldStop || !active) return;
+ setTimeout(poll, intervalMs);
+ };
+ poll();
+
+ return () => { active = false; };
+ }, [intervalMs, enabled]);
+}
diff --git a/frontend-react/src/hooks/useTheme.ts b/frontend-react/src/hooks/useTheme.ts
new file mode 100644
index 0000000..89d0c07
--- /dev/null
+++ b/frontend-react/src/hooks/useTheme.ts
@@ -0,0 +1,20 @@
+import { useState, useCallback, useEffect } from 'react';
+
+type Theme = 'light' | 'dark';
+
+export function useTheme() {
+ const [theme, setTheme] = useState(() => {
+ return (localStorage.getItem('theme') as Theme) || 'light';
+ });
+
+ useEffect(() => {
+ document.documentElement.className = theme;
+ localStorage.setItem('theme', theme);
+ }, [theme]);
+
+ const toggle = useCallback(() => {
+ setTheme((t) => (t === 'dark' ? 'light' : 'dark'));
+ }, []);
+
+ return { theme, toggle, isDark: theme === 'dark' };
+}
diff --git a/frontend-react/src/i18n/en.ts b/frontend-react/src/i18n/en.ts
new file mode 100644
index 0000000..9b8142c
--- /dev/null
+++ b/frontend-react/src/i18n/en.ts
@@ -0,0 +1,666 @@
+const en: Record = {
+ // ββ Sidebar ββ
+ 'nav.principal': 'Main',
+ 'nav.config': 'Configuration',
+ 'nav.admin': 'Administration',
+ 'nav.chat': 'Chat Agent',
+ 'nav.terraform': 'Terraform',
+ 'nav.promptGen': 'Prompt Generator',
+ 'nav.workspaces': 'Workspaces',
+ 'nav.explorer': 'OCI Explorer',
+ 'nav.reports': 'Reports',
+ 'nav.ociCreds': 'OCI Credentials',
+ 'nav.genai': 'GenAI Config',
+ 'nav.mcp': 'MCP Servers',
+ 'nav.adb': 'ADB Vector',
+ 'nav.embeddings': 'Embeddings',
+ 'nav.embConsult': 'Query Embeddings',
+ 'nav.users': 'Users',
+ 'nav.mfa': 'MFA',
+ 'nav.audit': 'Audit Log',
+ 'sidebar.lightMode': 'Light mode',
+ 'sidebar.darkMode': 'Dark mode',
+ 'sidebar.logout': 'Logout',
+ 'sidebar.roleAdmin': 'Administrator',
+ 'sidebar.roleUser': 'User',
+ 'sidebar.subtitle': 'Infrastructure & Security',
+
+ // ββ Login ββ
+ 'login.subtitle': 'Infrastructure & Security Engineer',
+ 'login.userPlaceholder': 'Username',
+ 'login.passPlaceholder': 'Password',
+ 'login.mfaScanQr': 'Scan the QR code with your authenticator app:',
+ 'login.totpPlaceholder': 'TOTP Code',
+ 'login.loading': 'Signing in...',
+ 'login.verify': 'Verify',
+ 'login.submit': 'Sign in',
+ 'login.error': 'Login failed',
+
+ // ββ Chat ββ
+ 'chat.timeNow': 'now',
+ 'chat.today': 'Today',
+ 'chat.yesterday': 'Yesterday',
+ 'chat.last7': 'Last 7 days',
+ 'chat.last30': 'Last 30 days',
+ 'chat.older': 'Older',
+ 'chat.other': 'Other',
+ 'chat.history': 'History',
+ 'chat.newChat': 'New Chat',
+ 'chat.untitled': 'Untitled',
+ 'chat.noConversations': 'No conversations yet',
+ 'chat.selectModel': 'Select a model...',
+ 'chat.savedConfigs': 'Saved Configs',
+ 'chat.searchModel': 'Search model...',
+ 'chat.noModels': 'No models found',
+ 'chat.ociCred': 'OCI Credential...',
+ 'chat.settings': 'Settings',
+ 'chat.newChatTitle': 'New chat',
+ 'chat.emptyTitle': 'Start a conversation with the agent.',
+ 'chat.emptySubtitle': 'Select a model to get started.',
+ 'chat.thinking': 'Thinking...',
+ 'chat.retry': 'Retry',
+ 'chat.attachFile': 'Attach file',
+ 'chat.placeholder': 'Type your message... (Shift+Enter for new line)',
+ 'chat.send': 'Send',
+ 'chat.selectOciCred': 'Select an OCI credential to use direct models.',
+ 'chat.sendError': 'Failed to send message',
+ 'chat.requestFailed': 'The request failed.',
+ 'chat.timeout': 'Timeout: the response is taking too long. Try again.',
+ 'chat.toolsAvailable': 'tools available',
+ 'chat.ociCredLabel': 'OCI Credential',
+ 'chat.select': 'Select...',
+ 'chat.analyzeFiles': 'Analyze the attached files.',
+
+ // ββ Terraform ββ
+ 'tf.history': 'History',
+ 'tf.newChat': 'New',
+ 'tf.noConversations': 'No conversations',
+ 'tf.searchModel': 'Search model...',
+ 'tf.selectModel': 'Select model...',
+ 'tf.savedConfigs': 'Saved Configs',
+ 'tf.ociConfig': 'OCI Config...',
+ 'tf.settings': 'Settings',
+ 'tf.newConversation': 'New conversation',
+ 'tf.emptyTitle': 'Describe the OCI infrastructure',
+ 'tf.emptySubtitle': 'The agent will generate Terraform code based on your description.',
+ 'tf.thinking': 'Processing...',
+ 'tf.retry': 'Retry',
+ 'tf.placeholder': 'Describe the desired infrastructure... (Shift+Enter for new line)',
+ 'tf.send': 'Send',
+ 'tf.workspace': 'Workspace',
+ 'tf.files': 'Files',
+ 'tf.terminal': 'Terminal',
+ 'tf.resources': 'Resources',
+ 'tf.plan': 'Plan',
+ 'tf.apply': 'Apply',
+ 'tf.destroy': 'Destroy',
+ 'tf.cancel': 'Cancel',
+ 'tf.download': 'Download ZIP',
+ 'tf.createWs': 'Create Workspace',
+ 'tf.noFiles': 'No files generated',
+ 'tf.timeout': 'Timeout: generation is taking too long.',
+ 'tf.selectOci': 'Select an OCI credential',
+ 'tf.compartment': 'Compartment',
+ 'tf.region': 'Region',
+ 'tf.rollback': 'Rollback',
+ 'tf.copy': 'Copy',
+ 'tf.copied': 'Copied',
+ 'tf.noWs': 'No active workspace',
+ 'tf.deleteWs': 'Delete',
+
+ // ββ Prompt Generator ββ
+ 'pg.history': 'History',
+ 'pg.new': 'New',
+ 'pg.noConversations': 'No conversations',
+ 'pg.emptyTitle': 'Describe the OCI infrastructure you need',
+ 'pg.emptySubtitle': 'The agent will generate a structured, optimized prompt for the Terraform Agent.',
+ 'pg.loading': 'Generating prompt...',
+ 'pg.placeholder': 'Describe the desired infrastructure... (Shift+Enter for new line)',
+ 'pg.copy': 'Copy',
+ 'pg.copied': 'Copied',
+ 'pg.retry': 'Retry',
+ 'pg.timeout': 'Timeout: generation is taking too long.',
+ 'pg.search': 'Search...',
+ 'pg.selectModel': 'Select model...',
+
+ // ββ Workspaces ββ
+ 'ws.title': 'Terraform Workspaces',
+ 'ws.all': 'All',
+ 'ws.active': 'Active',
+ 'ws.draft': 'Draft',
+ 'ws.failed': 'Failed',
+ 'ws.destroyed': 'Destroyed',
+ 'ws.loading': 'Loading...',
+ 'ws.refresh': 'Refresh',
+ 'ws.noWs': 'No workspaces found',
+ 'ws.noWsHint': 'Create workspaces via the Terraform Agent',
+ 'ws.loadingWs': 'Loading workspaces...',
+ 'ws.open': 'Open',
+ 'ws.plan': 'Plan',
+ 'ws.apply': 'Apply',
+ 'ws.destroy': 'Destroy',
+ 'ws.delete': 'Delete',
+ 'ws.cancel': 'Cancel',
+ 'ws.hideOutput': 'hide',
+ 'ws.viewOutput': 'view output',
+ 'ws.confirmDestroy': 'Confirm Destroy',
+ 'ws.confirmDestroyMsg': 'This action will destroy all resources provisioned by this workspace.',
+ 'ws.typeDestroy': 'Type DESTROY to confirm:',
+ 'ws.deleteWs': 'Delete Workspace',
+ 'ws.deleteWsMsg': 'Delete this workspace permanently? Local files will be removed.',
+ 'ws.noDate': 'No date',
+
+ // ββ Explorer ββ
+ 'exp.title': 'OCI Explorer',
+ 'exp.subtitle': 'Explore your Oracle Cloud Infrastructure resources',
+ 'exp.search': 'Search resource...',
+ 'exp.refresh': 'Refresh',
+ 'exp.loading': 'Loading...',
+ 'exp.noResources': 'No resources found',
+ 'exp.selectResource': 'Select a resource type',
+ 'exp.compartment': 'Compartment',
+ 'exp.region': 'Region',
+ 'exp.allRegions': 'All regions',
+ 'exp.start': 'Start',
+ 'exp.stop': 'Stop',
+ 'exp.details': 'Details',
+ 'exp.total': 'Total',
+ 'exp.network': 'Network',
+ 'exp.observability': 'Observability',
+ 'exp.security': 'Security',
+ 'exp.selectRegions': 'Select regions',
+ 'exp.nRegions': '{0} regions',
+ 'exp.all': 'All',
+ 'exp.none': 'None',
+ 'exp.noCompartment': 'No compartments found',
+ 'exp.selectConfig': 'Select an OCI config',
+ 'exp.loadingResources': 'Loading resources...',
+ 'exp.clearFilter': 'Try clearing the search filter',
+ 'exp.resources': 'resources',
+ 'exp.updating': 'updating...',
+ 'exp.regions': 'region(s)',
+ 'exp.filter': 'Filter...',
+ 'exp.startAction': 'Start',
+ 'exp.stopAction': 'Stop',
+ 'exp.updatingState': 'Updating...',
+ 'exp.createdAt': 'Created at',
+ 'exp.public': 'Public',
+ 'exp.yesPublic': 'Yes',
+ 'exp.noPrivate': 'No (private)',
+ 'exp.freeTier': 'Free Tier',
+ 'exp.yes': 'Yes',
+ 'exp.no': 'No',
+ 'exp.noRestrictions': 'No restrictions (open)',
+ 'exp.updateNetworkAccess': 'Update Network Access',
+ 'exp.accessUpdated': 'Access updated: ',
+ 'exp.objects': 'Objects',
+ 'exp.size': 'Size',
+ 'exp.visibility': 'Visibility',
+ 'exp.private': 'Private',
+ 'exp.rules': 'Rules',
+ 'exp.routes': 'Routes',
+ 'exp.hideRules': 'Hide rules',
+ 'exp.viewRules': 'View rules',
+ 'exp.add': 'Add',
+ 'exp.direction': 'Direction',
+ 'exp.protocol': 'Protocol',
+ 'exp.originDest': 'Source/Dest',
+ 'exp.ports': 'Ports',
+ 'exp.description': 'Description',
+ 'exp.portMin': 'Port Min',
+ 'exp.portMax': 'Port Max',
+ 'exp.myIp': 'My IP',
+ 'exp.save': 'Save',
+ 'exp.cancel': 'Cancel',
+ 'exp.ruleAdded': 'Rule added.',
+ 'exp.enterCidr': 'Enter the CIDR.',
+ 'exp.removeRule': 'Remove this rule?',
+ 'exp.removeNsgRule': 'Remove this NSG rule?',
+ 'exp.ipDetected': 'IP detected: {0} β adjust port and click Save',
+ 'exp.ipDetectFailed': 'Could not detect IP.',
+ 'exp.destination': 'Destination',
+ 'exp.destType': 'Dest Type',
+ 'exp.version': 'Version',
+ 'exp.ha': 'HA',
+
+ // ββ Reports ββ
+ 'rpt.title': 'CIS Reports',
+ 'rpt.subtitle': 'Run CIS OCI Benchmark compliance scans',
+ 'rpt.startScan': 'Start Scan',
+ 'rpt.running': 'Running...',
+ 'rpt.cancel': 'Cancel',
+ 'rpt.tenancy': 'Tenancy',
+ 'rpt.level': 'Level',
+ 'rpt.region': 'Region',
+ 'rpt.allRegions': 'All regions',
+ 'rpt.noReports': 'No reports found',
+ 'rpt.noReportsHint': 'Run a scan to generate compliance reports.',
+ 'rpt.progress': 'Progress',
+ 'rpt.completed': 'Completed',
+ 'rpt.failed': 'Failed',
+ 'rpt.viewReport': 'View Report',
+ 'rpt.download': 'Download',
+ 'rpt.delete': 'Delete',
+ 'rpt.summary': 'Summary',
+ 'rpt.passed': 'Passed',
+ 'rpt.findings': 'Findings',
+ 'rpt.recommendations': 'Recommendations',
+ 'rpt.selectTenancy': 'Select a tenancy',
+ 'rpt.selectCredential': 'Select a credential',
+ 'rpt.loadingReports': 'Loading reports...',
+ 'rpt.refresh': 'Refresh',
+ 'rpt.pageTitle': 'CIS Reports',
+ 'rpt.pageSubtitle': 'CIS Benchmark 3.0 β Compliance Scan & Dashboard',
+ 'rpt.runScanCis': 'Run CIS Scan',
+ 'rpt.inExecution': 'Running',
+ 'rpt.configParams': 'Configure the parameters and run the CIS compliance scan.',
+ 'rpt.ociCredential': 'OCI Credential',
+ 'rpt.selectOciCred': 'Select OCI credential...',
+ 'rpt.cisLevel': 'CIS Level',
+ 'rpt.level1Desc': 'Essential security checks',
+ 'rpt.level2Desc': 'Complete checks (includes Level 1)',
+ 'rpt.regions': 'Regions',
+ 'rpt.leaveEmpty': 'Leave empty to scan all subscribed regions',
+ 'rpt.executeCompliance': 'Run CIS Compliance',
+ 'rpt.scanProgress': 'Scan Progress',
+ 'rpt.hideLog': 'Hide log',
+ 'rpt.showLog': 'View full log',
+ 'rpt.lines': 'lines',
+ 'rpt.cancelScan': 'Cancel Scan',
+ 'rpt.selectReport': 'Select Report',
+ 'rpt.selectCompletedReport': 'Select a completed report...',
+ 'rpt.searchTenancy': 'Search tenancy...',
+ 'rpt.searchByTenancy': 'Search by tenancy...',
+ 'rpt.noCredentials': 'No credentials found',
+ 'rpt.allRegionsDefault': 'All regions (default)',
+ 'rpt.searchRegion': 'Search region...',
+ 'rpt.loadingDashboard': 'Loading dashboard...',
+ 'rpt.complianceScore': 'Compliance Score',
+ 'rpt.compliant': 'Compliant',
+ 'rpt.nonCompliant': 'Non-Compliant',
+ 'rpt.ofTotal': 'of total',
+ 'rpt.compliantItems': 'compliant items',
+ 'rpt.totalFindings': 'total findings',
+ 'rpt.evaluated': 'Evaluated',
+ 'rpt.controlsVerified': 'Controls verified',
+ 'rpt.regionsCount': 'region(s)',
+ 'rpt.complianceDist': 'Compliance Distribution',
+ 'rpt.findingsBySection': 'Findings by Section',
+ 'rpt.generatedAt': 'Generated at',
+ 'rpt.htmlReport': 'HTML Report',
+ 'rpt.files': 'Files',
+ 'rpt.inSections': 'sections',
+ 'rpt.fullEmbedding': 'Full Embedding',
+ 'rpt.noActiveTables': 'No active tables',
+ 'rpt.executionHistory': 'Execution History',
+ 'rpt.reports': 'report(s)',
+ 'rpt.loading': 'Loading...',
+ 'rpt.firstScanHint': 'Run the first CIS scan above.',
+ 'rpt.selected': 'Selected',
+ 'rpt.deleteConfirm': 'Delete this report?',
+ 'rpt.deleteFailed': 'Failed to delete',
+ 'rpt.selectOciCredError': 'Select an OCI credential',
+ 'rpt.alreadyRunning': 'A scan is already running',
+ 'rpt.errorLoadReports': 'Error loading reports',
+ 'rpt.errorStartScan': 'Error starting scan',
+ 'rpt.selectAdb': 'Select an ADB connection.',
+ 'rpt.selectTable': 'Select a table.',
+ 'rpt.errorEmbedding': 'Error generating embedding',
+ 'rpt.cancelled': 'Cancelled',
+ 'rpt.completedAt': 'Completed:',
+
+ // ββ Downloads ββ
+ 'dl.title': 'Downloads',
+ 'dl.subtitle': 'Download generated CIS reports',
+ 'dl.allTenancies': 'All Tenancies',
+ 'dl.allSections': 'All sections',
+ 'dl.reports': 'report(s)',
+ 'dl.refresh': 'Refresh',
+ 'dl.loadError': 'Error loading reports',
+ 'dl.loading': 'Loading reports...',
+ 'dl.noReports': 'No completed reports.',
+ 'dl.noReportsHint': 'Run a scan in the Reports tab to generate reports.',
+ 'dl.files': 'file(s)',
+ 'dl.hide': 'Hide',
+ 'dl.viewFiles': 'View Files',
+ 'dl.loadingFiles': 'Loading files...',
+ 'dl.noFiles': 'No files found',
+ 'dl.filesInSections': 'file(s) in {0} sections',
+
+ // ββ OCI Config ββ
+ 'oci.title': 'OCI Credentials',
+ 'oci.subtitle': 'Manage Oracle Cloud Infrastructure credentials',
+ 'oci.registered': 'Registered Credentials',
+ 'oci.credential': 'credential',
+ 'oci.credentials': 'credentials',
+ 'oci.noCredentials': 'No credentials registered.',
+ 'oci.edit': 'Edit',
+ 'oci.test': 'Test',
+ 'oci.delete': 'Delete',
+ 'oci.yes': 'Yes',
+ 'oci.no': 'No',
+ 'oci.newCredential': 'New Credential',
+ 'oci.editCredential': 'Edit Credential',
+ 'oci.addDesc': 'Add your OCI account credentials to run reports and access services.',
+ 'oci.editing': 'Editing:',
+ 'oci.tenancyName': 'Tenancy Name',
+ 'oci.ocidTenancy': 'OCID Tenancy',
+ 'oci.region': 'Region',
+ 'oci.selectRegion': 'Select region...',
+ 'oci.searchRegion': 'Search region...',
+ 'oci.noRegion': 'No regions found',
+ 'oci.compartment': 'Compartment OCID',
+ 'oci.ocidUser': 'OCID User',
+ 'oci.fingerprint': 'Fingerprint',
+ 'oci.sessionToken': 'Session Token',
+ 'oci.sessionTokenHint': 'Paste the token file content generated by',
+ 'oci.sessionKeyLabel': 'Session Key (.pem)',
+ 'oci.privateKey': 'Private Key (.pem)',
+ 'oci.publicKey': 'Public Key (.pem)',
+ 'oci.keyPassphrase': 'Key Passphrase',
+ 'oci.keyPassphraseHint': 'Only if the private key is password protected',
+ 'oci.sshPublicKey': 'SSH Public Key',
+ 'oci.sshPublicKeyHint': 'SSH key for compute instance access (e.g. ssh-rsa AAAA...)',
+ 'oci.saveCredential': 'Save Credential',
+ 'oci.saveChanges': 'Save Changes',
+ 'oci.cancel': 'Cancel',
+ 'oci.saving': 'Saving credential...',
+ 'oci.saved': 'Credential saved successfully!',
+ 'oci.updated': 'Credential updated successfully!',
+ 'oci.deleting': 'Deleting credential...',
+ 'oci.deleted': 'Credential deleted successfully!',
+ 'oci.fillField': 'Fill in',
+ 'oci.fillOcidTenancy': 'Fill in OCID Tenancy',
+ 'oci.fillUserAndFP': 'Fill in OCID User and Fingerprint',
+ 'oci.pasteToken': 'Paste the Session Token',
+ 'oci.selectKey': 'Select the private key (.pem)',
+ 'oci.selectSessionKey': 'Select the session key (.pem)',
+ 'oci.changeHint': 'Fill to change. Current:',
+ 'oci.keepKey': 'Leave empty to keep current key',
+ 'oci.typeCannotChange': '(type cannot be changed)',
+ 'oci.sshNotConfigured': 'not configured',
+
+ // ββ GenAI Config ββ
+ 'genai.title': 'GenAI Models',
+ 'genai.subtitle': 'Configure generative AI models for the Chat Agent',
+ 'genai.configured': 'Configured Models',
+ 'genai.model': 'model',
+ 'genai.models': 'models',
+ 'genai.noModels': 'No models configured.',
+ 'genai.newModel': 'New GenAI Model',
+ 'genai.editModel': 'Edit Model',
+ 'genai.configName': 'Config Name',
+ 'genai.ociCredential': 'OCI Credential',
+ 'genai.modelSelect': 'Model',
+ 'genai.genaiRegion': 'GenAI Region',
+ 'genai.compartment': 'Compartment OCID',
+ 'genai.modelOcid': 'Model OCID',
+ 'genai.modelOcidHint': 'Paste the model OCID from OCI portal (optional)',
+ 'genai.customOcid': 'Custom (use OCID)',
+ 'genai.default': 'Default Model',
+ 'genai.setDefault': 'Set as Default',
+ 'genai.saveModel': 'Save Model',
+ 'genai.saveChanges': 'Save Changes',
+ 'genai.cancel': 'Cancel',
+ 'genai.saving': 'Saving model...',
+ 'genai.deleting': 'Deleting model...',
+ 'genai.addDesc': 'Add custom models or create presets with specific credentials.',
+ 'genai.fillName': 'Fill in config name',
+ 'genai.selectOci': 'Select an OCI credential',
+ 'genai.fillOcid': 'Enter Model OCID for custom model',
+
+ // ββ MCP Servers ββ
+ 'mcp.title': 'MCP Servers',
+ 'mcp.subtitle': 'MCP servers for tool integration and task execution',
+ 'mcp.registered': 'Registered Servers',
+ 'mcp.noServers': 'No MCP servers registered.',
+ 'mcp.edit': 'Edit',
+ 'mcp.activate': 'Activate',
+ 'mcp.deactivate': 'Deactivate',
+ 'mcp.active': 'Active',
+ 'mcp.inactive': 'Inactive',
+ 'mcp.delete': 'Delete',
+ 'mcp.yes': 'Yes',
+ 'mcp.no': 'No',
+ 'mcp.command': 'Command:',
+ 'mcp.url': 'URL:',
+ 'mcp.module': 'Module:',
+ 'mcp.adb': 'ADB:',
+ 'mcp.noAdb': 'None',
+ 'mcp.tools': 'Tools',
+ 'mcp.noTools': 'No tools registered',
+ 'mcp.discoverTools': 'Discover Tools',
+ 'mcp.discovering': 'Connecting to MCP server to discover tools...',
+ 'mcp.newServer': 'Register New Server',
+ 'mcp.editServer': 'Edit Server',
+ 'mcp.name': 'Name',
+ 'mcp.description': 'Description',
+ 'mcp.serverType': 'Server Type',
+ 'mcp.commandLabel': 'Command',
+ 'mcp.commandHint': 'Command to start the server',
+ 'mcp.argsLabel': 'Arguments (JSON)',
+ 'mcp.argsHint': 'Array of arguments passed to the command',
+ 'mcp.urlLabel': 'Server URL',
+ 'mcp.urlHint': 'MCP server SSE endpoint',
+ 'mcp.linkAdb': 'Link ADB Vector',
+ 'mcp.linkAdbHint': 'Optional β the MCP server can use this database as a tool.',
+ 'mcp.saveServer': 'Register Server',
+ 'mcp.saveChanges': 'Save Changes',
+ 'mcp.cancel': 'Cancel',
+ 'mcp.saving': 'Registering server...',
+ 'mcp.updating': 'Updating server...',
+ 'mcp.fillName': 'Fill in server name',
+ 'mcp.invalidArgs': 'Arguments must be a valid JSON array',
+ 'mcp.configDesc': 'Configure a new MCP server. Fields adjust based on the selected type.',
+
+ // ββ ADB Config ββ
+ 'adb.title': 'ADB Connections',
+ 'adb.subtitle': 'Oracle Autonomous Database connections via Wallet (mTLS)',
+ 'adb.registered': 'Registered Connections',
+ 'adb.connection': 'connection',
+ 'adb.connections': 'connections',
+ 'adb.noConnections': 'No ADB connections registered.',
+ 'adb.edit': 'Edit',
+ 'adb.test': 'Test',
+ 'adb.delete': 'Delete',
+ 'adb.yes': 'Yes',
+ 'adb.no': 'No',
+ 'adb.newConnection': 'New Connection',
+ 'adb.editConnection': 'Edit Connection',
+ 'adb.connName': 'Connection Name',
+ 'adb.walletZip': 'Wallet ZIP',
+ 'adb.walletHint': 'Upload wallet to extract DSNs from tnsnames.ora',
+ 'adb.walletEditHint': 'Upload new wallet or leave empty to keep current',
+ 'adb.analyze': 'Analyze',
+ 'adb.dsn': 'DSN (TNS Name)',
+ 'adb.dsnHint': 'Select or type the DSN. E.g. myatp_high',
+ 'adb.username': 'Username',
+ 'adb.password': 'Password',
+ 'adb.walletPassword': 'Wallet Password',
+ 'adb.genaiConfig': 'GenAI Config (Embeddings)',
+ 'adb.genaiConfigHint': 'OCI credentials used to generate embeddings via GenAI.',
+ 'adb.noRag': 'None (no RAG)',
+ 'adb.embeddingModel': 'Embedding Model',
+ 'adb.embeddingModelHint': 'OCI GenAI model to generate vectors.',
+ 'adb.saveConnection': 'Save Connection',
+ 'adb.saveChanges': 'Save Changes',
+ 'adb.cancel': 'Cancel',
+ 'adb.saving': 'Saving connection...',
+ 'adb.deleting': 'Deleting connection...',
+ 'adb.updateWallet': 'Update Wallet',
+ 'adb.adbConnection': 'ADB Connection',
+ 'adb.uploadWallet': 'Upload Wallet',
+ 'adb.connDesc': 'Connection via python-oracledb Thin mode with Wallet (mTLS) to Oracle Autonomous Database.',
+ 'adb.selectWallet': 'Select a wallet ZIP file.',
+ 'adb.selectConnection': 'Select an ADB connection.',
+ 'adb.fillName': 'Fill in connection name',
+ 'adb.fillDsn': 'Fill in DSN',
+ 'adb.fillUsername': 'Fill in username',
+ 'adb.fillPassword': 'Fill in password',
+
+ // ββ Embeddings ββ
+ 'emb.title': 'Embeddings',
+ 'emb.subtitle': 'Vector knowledge base β upload, import and management',
+ 'emb.noAdb': 'No ADB connection configured.',
+ 'emb.noAdbHint': 'Configure in ADB Vector.',
+ 'emb.noOci': 'No OCI credential configured.',
+ 'emb.noOciHint': 'Configure in Config β OCI.',
+ 'emb.existing': 'Existing Embeddings',
+ 'emb.existingDesc': 'Query documents stored in ADB embedding tables.',
+ 'emb.adbConnection': 'ADB Connection',
+ 'emb.table': 'Table',
+ 'emb.load': 'Load',
+ 'emb.noEmbeddings': 'No embeddings found.',
+ 'emb.total': 'Total: {0} documents',
+ 'emb.cisTitle': 'CIS Recommendations',
+ 'emb.cisDesc': 'Upload the latest PDF version to keep CIS Oracle Cloud recommendations updated in the vector database.',
+ 'emb.pdfFile': 'PDF File',
+ 'emb.uploadCis': 'Upload CIS Recommendations',
+ 'emb.kbTitle': 'Knowledge Base',
+ 'emb.kbDesc': 'Upload documents to feed the team knowledge base. Files will be vectorized and available for chat queries.',
+ 'emb.fileLabel': 'File (.txt, .pdf, .csv, .json, .md)',
+ 'emb.uploadFile': 'Upload File',
+ 'emb.urlLabel': 'URL (web page or PDF)',
+ 'emb.importUrl': 'Import URL',
+ 'emb.purgeTitle': 'Purge & Re-embed',
+ 'emb.purgeDesc': 'Clear embeddings from a table to free space or prepare for re-ingestion. This action is irreversible.',
+ 'emb.tenancyOptional': 'Tenancy (optional)',
+ 'emb.purge': 'Purge',
+ 'emb.confirm': 'Confirm',
+ 'emb.selectTable': 'Select a table.',
+ 'emb.selectAdbAndFile': 'Select ADB connection and file.',
+ 'emb.selectAdbAndUrl': 'Select ADB connection and enter URL.',
+ 'emb.processing': 'Processing PDF and generating embeddings...',
+ 'emb.uploading': 'Uploading and generating embeddings...',
+ 'emb.fetchingUrl': 'Fetching content and generating embeddings...',
+ 'emb.purging': 'Clearing embeddings...',
+
+ // ββ EmbConsult ββ
+ 'ec.title': 'Query Embeddings',
+ 'ec.subtitle': 'Ask about data stored in vector databases',
+ 'ec.clear': 'Clear',
+ 'ec.tableOptional': 'Table (optional)',
+ 'ec.allTables': 'All tables',
+ 'ec.emptyTitle': 'Make a query to search embeddings.',
+ 'ec.emptyHint': 'E.g. "findings for CIS 1.1", "networking recommendations", "how to fix MFA"',
+ 'ec.searching': 'Querying vector databases...',
+ 'ec.placeholder': 'Type your query...',
+ 'ec.submit': 'Query',
+ 'ec.sources': 'Consulted sources',
+ 'ec.noAnswer': 'No model response.',
+
+ // ββ Users ββ
+ 'usr.title': 'Users',
+ 'usr.activeCount': 'active',
+ 'usr.activesCount': 'active',
+ 'usr.inactiveCount': 'inactive',
+ 'usr.inactivesCount': 'inactive',
+ 'usr.total': 'total',
+ 'usr.newUser': 'New User',
+ 'usr.cancel': 'Cancel',
+ 'usr.createUser': 'Create New User',
+ 'usr.firstName': 'First Name',
+ 'usr.lastName': 'Last Name',
+ 'usr.username': 'Username',
+ 'usr.email': 'Email',
+ 'usr.password': 'Password',
+ 'usr.role': 'Role',
+ 'usr.creating': 'Creating...',
+ 'usr.create': 'Create User',
+ 'usr.noUsers': 'No users registered',
+ 'usr.noUsersHint': 'Click "New User" to create the first user.',
+ 'usr.name': 'Name',
+ 'usr.mfa': 'MFA',
+ 'usr.status': 'Status',
+ 'usr.lastLogin': 'Last Login',
+ 'usr.actions': 'Actions',
+ 'usr.active': 'Active',
+ 'usr.inactive': 'Inactive',
+ 'usr.never': 'Never',
+ 'usr.confirm': 'Confirm',
+ 'usr.deactivate': 'Deactivate user',
+ 'usr.created': 'User created successfully!',
+ 'usr.roleUpdated': 'Role updated',
+ 'usr.deactivated': 'User deactivated',
+ 'usr.requiredName': 'First and last name are required',
+ 'usr.requiredUsername': 'Username is required',
+ 'usr.requiredPassword': 'Password is required',
+
+ // ββ MFA ββ
+ 'mfa.title': 'MFA Authentication (TOTP)',
+ 'mfa.subtitle': 'Protect your account with two-factor authentication via Google Authenticator or Authy.',
+ 'mfa.enabled': 'MFA is enabled',
+ 'mfa.disabled': 'MFA is disabled',
+ 'mfa.enabledDesc': 'Your account is protected with two-factor authentication.',
+ 'mfa.disabledDesc': 'Enable MFA to add an extra layer of security.',
+ 'mfa.disableHint': 'To disable MFA, click the button below. This will reduce your account security.',
+ 'mfa.disableBtn': 'Disable MFA',
+ 'mfa.confirmDisable': 'Are you sure you want to disable MFA for your account?',
+ 'mfa.scanQr': 'Scan the QR Code or copy the secret',
+ 'mfa.openApp': 'Open your authenticator app (Google Authenticator, Authy, etc.)',
+ 'mfa.enterCode': 'Enter the code from your app',
+ 'mfa.activateMfa': 'Activate MFA',
+ 'mfa.generateSecret': 'Generate Secret',
+ 'mfa.generateHint': 'Click the button below to generate a TOTP secret. You will need an authenticator app to scan the QR Code.',
+ 'mfa.activated': 'MFA activated successfully!',
+ 'mfa.deactivated': 'MFA deactivated.',
+ 'mfa.invalidCode': 'Enter a 6-digit code',
+
+ // ββ Audit ββ
+ 'audit.title': 'Audit Log',
+ 'audit.subtitle': 'System activity monitoring, configurations and chat sessions.',
+ 'audit.tabAudit': 'Audit Log',
+ 'audit.tabConfig': 'Config Logs',
+ 'audit.tabChat': 'Chat Logs',
+ 'audit.records': 'records',
+ 'audit.refresh': 'Refresh',
+ 'audit.all': 'All',
+ 'audit.errors': 'Errors',
+ 'audit.success': 'Success',
+ 'audit.info': 'Info',
+ 'audit.loadingLogs': 'Loading logs...',
+ 'audit.noAudit': 'No audit records.',
+ 'audit.noConfig': 'No configuration logs recorded.',
+ 'audit.noChat': 'No chat logs recorded.',
+ 'audit.date': 'Date',
+ 'audit.user': 'User',
+ 'audit.action': 'Action',
+ 'audit.resource': 'Resource',
+ 'audit.details': 'Details',
+ 'audit.ip': 'IP',
+ 'audit.config': 'Config',
+ 'audit.status': 'Status',
+ 'audit.message': 'Message',
+ 'audit.session': 'Session',
+ 'audit.source': 'Source',
+
+ // ββ Common ββ
+ 'common.save': 'Save',
+ 'common.cancel': 'Cancel',
+ 'common.delete': 'Delete',
+ 'common.edit': 'Edit',
+ 'common.create': 'Create',
+ 'common.update': 'Refresh',
+ 'common.close': 'Close',
+ 'common.confirm': 'Confirm',
+ 'common.back': 'Back',
+ 'common.search': 'Search',
+ 'common.refresh': 'Refresh',
+ 'common.download': 'Download',
+ 'common.upload': 'Upload',
+ 'common.copy': 'Copy',
+ 'common.retry': 'Retry',
+ 'common.send': 'Send',
+ 'common.clear': 'Clear',
+ 'common.test': 'Test',
+ 'common.yes': 'Yes',
+ 'common.no': 'No',
+ 'common.loading': 'Loading...',
+ 'common.saving': 'Saving...',
+ 'common.select': 'Select...',
+ 'common.noData': 'No data found',
+ 'common.error': 'Error',
+ 'common.success': 'Success',
+};
+
+export default en;
diff --git a/frontend-react/src/i18n/index.ts b/frontend-react/src/i18n/index.ts
new file mode 100644
index 0000000..1e8d188
--- /dev/null
+++ b/frontend-react/src/i18n/index.ts
@@ -0,0 +1,31 @@
+import { create } from 'zustand';
+import pt from './pt';
+import en from './en';
+
+type Lang = 'pt' | 'en';
+
+const LANGS: Record> = { pt, en };
+
+function detectLang(): Lang {
+ const saved = localStorage.getItem('lang') as Lang | null;
+ if (saved && LANGS[saved]) return saved;
+ return navigator.language.startsWith('pt') ? 'pt' : 'en';
+}
+
+interface I18nState {
+ lang: Lang;
+ setLang: (lang: Lang) => void;
+ t: (key: string) => string;
+}
+
+export const useI18n = create((set, get) => ({
+ lang: detectLang(),
+ setLang: (lang: Lang) => {
+ localStorage.setItem('lang', lang);
+ set({ lang });
+ },
+ t: (key: string) => {
+ const { lang } = get();
+ return LANGS[lang]?.[key] ?? LANGS.pt[key] ?? key;
+ },
+}));
diff --git a/frontend-react/src/i18n/pt.ts b/frontend-react/src/i18n/pt.ts
new file mode 100644
index 0000000..086fa8b
--- /dev/null
+++ b/frontend-react/src/i18n/pt.ts
@@ -0,0 +1,666 @@
+const pt: Record = {
+ // ββ Sidebar ββ
+ 'nav.principal': 'Principal',
+ 'nav.config': 'ConfiguraΓ§Γ£o',
+ 'nav.admin': 'AdministraΓ§Γ£o',
+ 'nav.chat': 'Chat Agent',
+ 'nav.terraform': 'Terraform',
+ 'nav.promptGen': 'Prompt Generator',
+ 'nav.workspaces': 'Workspaces',
+ 'nav.explorer': 'OCI Explorer',
+ 'nav.reports': 'RelatΓ³rios',
+ 'nav.ociCreds': 'Credenciais OCI',
+ 'nav.genai': 'GenAI Config',
+ 'nav.mcp': 'MCP Servers',
+ 'nav.adb': 'ADB Vector',
+ 'nav.embeddings': 'Embeddings',
+ 'nav.embConsult': 'Consultar Embeddings',
+ 'nav.users': 'UsuΓ‘rios',
+ 'nav.mfa': 'MFA',
+ 'nav.audit': 'Audit Log',
+ 'sidebar.lightMode': 'Modo claro',
+ 'sidebar.darkMode': 'Modo escuro',
+ 'sidebar.logout': 'Sair',
+ 'sidebar.roleAdmin': 'Administrador',
+ 'sidebar.roleUser': 'UsuΓ‘rio',
+ 'sidebar.subtitle': 'Infrastructure & Security',
+
+ // ββ Login ββ
+ 'login.subtitle': 'Infrastructure & Security Engineer',
+ 'login.userPlaceholder': 'Usuario',
+ 'login.passPlaceholder': 'Senha',
+ 'login.mfaScanQr': 'Escaneie o QR code no seu app autenticador:',
+ 'login.totpPlaceholder': 'Codigo TOTP',
+ 'login.loading': 'Entrando...',
+ 'login.verify': 'Verificar',
+ 'login.submit': 'Entrar',
+ 'login.error': 'Erro ao fazer login',
+
+ // ββ Chat ββ
+ 'chat.timeNow': 'agora',
+ 'chat.today': 'Hoje',
+ 'chat.yesterday': 'Ontem',
+ 'chat.last7': 'Ultimos 7 dias',
+ 'chat.last30': 'Ultimos 30 dias',
+ 'chat.older': 'Anteriores',
+ 'chat.other': 'Outras',
+ 'chat.history': 'Historico',
+ 'chat.newChat': 'Nova Conversa',
+ 'chat.untitled': 'Sem titulo',
+ 'chat.noConversations': 'Nenhuma conversa ainda',
+ 'chat.selectModel': 'Selecione um modelo...',
+ 'chat.savedConfigs': 'Configs Salvas',
+ 'chat.searchModel': 'Buscar modelo...',
+ 'chat.noModels': 'Nenhum modelo encontrado',
+ 'chat.ociCred': 'Credencial OCI...',
+ 'chat.settings': 'Configuracoes',
+ 'chat.newChatTitle': 'Nova conversa',
+ 'chat.emptyTitle': 'Inicie uma conversa com o agente.',
+ 'chat.emptySubtitle': 'Selecione um modelo para comecar.',
+ 'chat.thinking': 'Pensando...',
+ 'chat.retry': 'Reenviar',
+ 'chat.attachFile': 'Anexar arquivo',
+ 'chat.placeholder': 'Digite sua mensagem... (Shift+Enter para nova linha)',
+ 'chat.send': 'Enviar',
+ 'chat.selectOciCred': 'Selecione uma credencial OCI para usar modelos diretos.',
+ 'chat.sendError': 'Falha ao enviar mensagem',
+ 'chat.requestFailed': 'A requisicao falhou.',
+ 'chat.timeout': 'Timeout: a resposta esta demorando muito. Tente novamente.',
+ 'chat.toolsAvailable': 'tools disponiveis',
+ 'chat.ociCredLabel': 'Credencial OCI',
+ 'chat.select': 'Selecionar...',
+ 'chat.analyzeFiles': 'Analise os arquivos anexados.',
+
+ // ββ Terraform ββ
+ 'tf.history': 'Historico',
+ 'tf.newChat': 'Nova',
+ 'tf.noConversations': 'Nenhuma conversa',
+ 'tf.searchModel': 'Buscar modelo...',
+ 'tf.selectModel': 'Selecione modelo...',
+ 'tf.savedConfigs': 'Configs Salvas',
+ 'tf.ociConfig': 'OCI Config...',
+ 'tf.settings': 'Configuracoes',
+ 'tf.newConversation': 'Nova conversa',
+ 'tf.emptyTitle': 'Descreva a infraestrutura OCI',
+ 'tf.emptySubtitle': 'O agente vai gerar codigo Terraform baseado na sua descricao.',
+ 'tf.thinking': 'Processando...',
+ 'tf.retry': 'Reenviar',
+ 'tf.placeholder': 'Descreva a infraestrutura desejada... (Shift+Enter para nova linha)',
+ 'tf.send': 'Enviar',
+ 'tf.workspace': 'Workspace',
+ 'tf.files': 'Arquivos',
+ 'tf.terminal': 'Terminal',
+ 'tf.resources': 'Recursos',
+ 'tf.plan': 'Plan',
+ 'tf.apply': 'Apply',
+ 'tf.destroy': 'Destroy',
+ 'tf.cancel': 'Cancelar',
+ 'tf.download': 'Download ZIP',
+ 'tf.createWs': 'Criar Workspace',
+ 'tf.noFiles': 'Nenhum arquivo gerado',
+ 'tf.timeout': 'Timeout: a geracao esta demorando muito.',
+ 'tf.selectOci': 'Selecione uma credencial OCI',
+ 'tf.compartment': 'Compartimento',
+ 'tf.region': 'Regiao',
+ 'tf.rollback': 'Rollback',
+ 'tf.copy': 'Copiar',
+ 'tf.copied': 'Copiado',
+ 'tf.noWs': 'Nenhum workspace ativo',
+ 'tf.deleteWs': 'Excluir',
+
+ // ββ Prompt Generator ββ
+ 'pg.history': 'Historico',
+ 'pg.new': 'Nova',
+ 'pg.noConversations': 'Nenhuma conversa',
+ 'pg.emptyTitle': 'Descreva a infraestrutura OCI que voce precisa',
+ 'pg.emptySubtitle': 'O agente vai gerar um prompt estruturado e otimizado para o Terraform Agent.',
+ 'pg.loading': 'Gerando prompt...',
+ 'pg.placeholder': 'Descreva a infraestrutura desejada... (Shift+Enter para nova linha)',
+ 'pg.copy': 'Copiar',
+ 'pg.copied': 'Copiado',
+ 'pg.retry': 'Reenviar',
+ 'pg.timeout': 'Timeout: a geracao esta demorando muito.',
+ 'pg.search': 'Buscar...',
+ 'pg.selectModel': 'Selecione modelo...',
+
+ // ββ Workspaces ββ
+ 'ws.title': 'Terraform Workspaces',
+ 'ws.all': 'Todos',
+ 'ws.active': 'Ativos',
+ 'ws.draft': 'Draft',
+ 'ws.failed': 'Falha',
+ 'ws.destroyed': 'DestruΓdos',
+ 'ws.loading': 'Carregando...',
+ 'ws.refresh': 'Atualizar',
+ 'ws.noWs': 'Nenhum workspace encontrado',
+ 'ws.noWsHint': 'Crie workspaces pelo Terraform Agent',
+ 'ws.loadingWs': 'Carregando workspaces...',
+ 'ws.open': 'Abrir',
+ 'ws.plan': 'Plan',
+ 'ws.apply': 'Apply',
+ 'ws.destroy': 'Destroy',
+ 'ws.delete': 'Excluir',
+ 'ws.cancel': 'Cancelar',
+ 'ws.hideOutput': 'ocultar',
+ 'ws.viewOutput': 'ver output',
+ 'ws.confirmDestroy': 'Confirmar Destroy',
+ 'ws.confirmDestroyMsg': 'Esta acao ira destruir todos os recursos provisionados por este workspace.',
+ 'ws.typeDestroy': 'Digite DESTROY para confirmar:',
+ 'ws.deleteWs': 'Excluir Workspace',
+ 'ws.deleteWsMsg': 'Deseja excluir este workspace permanentemente? Arquivos locais serao removidos.',
+ 'ws.noDate': 'Sem data',
+
+ // ββ Explorer ββ
+ 'exp.title': 'OCI Explorer',
+ 'exp.subtitle': 'Explore recursos da sua infraestrutura Oracle Cloud',
+ 'exp.search': 'Buscar recurso...',
+ 'exp.refresh': 'Atualizar',
+ 'exp.loading': 'Carregando...',
+ 'exp.noResources': 'Nenhum recurso encontrado',
+ 'exp.selectResource': 'Selecione um tipo de recurso',
+ 'exp.compartment': 'Compartimento',
+ 'exp.region': 'Regiao',
+ 'exp.allRegions': 'Todas as regioes',
+ 'exp.start': 'Start',
+ 'exp.stop': 'Stop',
+ 'exp.details': 'Detalhes',
+ 'exp.total': 'Total',
+ 'exp.network': 'Rede',
+ 'exp.observability': 'Observabilidade',
+ 'exp.security': 'Seguranca',
+ 'exp.selectRegions': 'Selecionar regioes',
+ 'exp.nRegions': '{0} regioes',
+ 'exp.all': 'Todas',
+ 'exp.none': 'Nenhuma',
+ 'exp.noCompartment': 'Nenhum compartment encontrado',
+ 'exp.selectConfig': 'Selecione uma config OCI',
+ 'exp.loadingResources': 'Carregando recursos...',
+ 'exp.clearFilter': 'Tente limpar o filtro de busca',
+ 'exp.resources': 'recursos',
+ 'exp.updating': 'atualizando...',
+ 'exp.regions': 'regiao(oes)',
+ 'exp.filter': 'Filtrar...',
+ 'exp.startAction': 'Iniciar',
+ 'exp.stopAction': 'Parar',
+ 'exp.updatingState': 'Atualizando...',
+ 'exp.createdAt': 'Criado em',
+ 'exp.public': 'Publico',
+ 'exp.yesPublic': 'Sim',
+ 'exp.noPrivate': 'Nao (privada)',
+ 'exp.freeTier': 'Free Tier',
+ 'exp.yes': 'Sim',
+ 'exp.no': 'Nao',
+ 'exp.noRestrictions': 'Sem restricoes (aberto)',
+ 'exp.updateNetworkAccess': 'Atualizar Acesso de Rede',
+ 'exp.accessUpdated': 'Acesso atualizado: ',
+ 'exp.objects': 'Objetos',
+ 'exp.size': 'Tamanho',
+ 'exp.visibility': 'Visibilidade',
+ 'exp.private': 'Privado',
+ 'exp.rules': 'Regras',
+ 'exp.routes': 'Rotas',
+ 'exp.hideRules': 'Ocultar regras',
+ 'exp.viewRules': 'Ver regras',
+ 'exp.add': 'Adicionar',
+ 'exp.direction': 'Direcao',
+ 'exp.protocol': 'Protocolo',
+ 'exp.originDest': 'Origem/Destino',
+ 'exp.ports': 'Portas',
+ 'exp.description': 'Descricao',
+ 'exp.portMin': 'Porta Min',
+ 'exp.portMax': 'Porta Max',
+ 'exp.myIp': 'Meu IP',
+ 'exp.save': 'Salvar',
+ 'exp.cancel': 'Cancelar',
+ 'exp.ruleAdded': 'Regra adicionada.',
+ 'exp.enterCidr': 'Informe o CIDR.',
+ 'exp.removeRule': 'Remover esta regra?',
+ 'exp.removeNsgRule': 'Remover esta regra do NSG?',
+ 'exp.ipDetected': 'IP detectado: {0} β ajuste porta e clique Salvar',
+ 'exp.ipDetectFailed': 'Nao foi possivel detectar o IP.',
+ 'exp.destination': 'Destino',
+ 'exp.destType': 'Tipo Destino',
+ 'exp.version': 'Versao',
+ 'exp.ha': 'HA',
+
+ // ββ Reports ββ
+ 'rpt.title': 'CIS Reports',
+ 'rpt.subtitle': 'Execute scans de conformidade CIS OCI Benchmark',
+ 'rpt.startScan': 'Iniciar Scan',
+ 'rpt.running': 'Executando...',
+ 'rpt.cancel': 'Cancelar',
+ 'rpt.tenancy': 'Tenancy',
+ 'rpt.level': 'Level',
+ 'rpt.region': 'Regiao',
+ 'rpt.allRegions': 'Todas as regioes',
+ 'rpt.noReports': 'Nenhum relatorio encontrado',
+ 'rpt.noReportsHint': 'Execute um scan para gerar relatorios de conformidade.',
+ 'rpt.progress': 'Progresso',
+ 'rpt.completed': 'Concluido',
+ 'rpt.failed': 'Falhou',
+ 'rpt.viewReport': 'Ver Relatorio',
+ 'rpt.download': 'Download',
+ 'rpt.delete': 'Excluir',
+ 'rpt.summary': 'Resumo',
+ 'rpt.passed': 'Passou',
+ 'rpt.findings': 'Findings',
+ 'rpt.recommendations': 'Recomendacoes',
+ 'rpt.selectTenancy': 'Selecione uma tenancy',
+ 'rpt.selectCredential': 'Selecione uma credencial',
+ 'rpt.loadingReports': 'Carregando relatorios...',
+ 'rpt.refresh': 'Atualizar',
+ 'rpt.pageTitle': 'Relatorios CIS',
+ 'rpt.pageSubtitle': 'CIS Benchmark 3.0 β Compliance Scan & Dashboard',
+ 'rpt.runScanCis': 'Executar Scan CIS',
+ 'rpt.inExecution': 'Em execucao',
+ 'rpt.configParams': 'Configure os parametros e execute o scan de compliance CIS.',
+ 'rpt.ociCredential': 'Credencial OCI',
+ 'rpt.selectOciCred': 'Selecione a credencial OCI...',
+ 'rpt.cisLevel': 'CIS Level',
+ 'rpt.level1Desc': 'Verificacoes essenciais de seguranca',
+ 'rpt.level2Desc': 'Verificacoes completas (inclui Level 1)',
+ 'rpt.regions': 'Regioes',
+ 'rpt.leaveEmpty': 'Deixe vazio para escanear todas as regioes subscritas',
+ 'rpt.executeCompliance': 'Executar CIS Compliance',
+ 'rpt.scanProgress': 'Progresso do Scan',
+ 'rpt.hideLog': 'Ocultar log',
+ 'rpt.showLog': 'Ver log completo',
+ 'rpt.lines': 'linhas',
+ 'rpt.cancelScan': 'Cancelar Scan',
+ 'rpt.selectReport': 'Selecionar Relatorio',
+ 'rpt.selectCompletedReport': 'Selecione um relatorio concluido...',
+ 'rpt.searchTenancy': 'Buscar tenancy...',
+ 'rpt.searchByTenancy': 'Buscar por tenancy...',
+ 'rpt.noCredentials': 'Nenhuma credencial encontrada',
+ 'rpt.allRegionsDefault': 'Todas as regioes (padrao)',
+ 'rpt.searchRegion': 'Buscar regiao...',
+ 'rpt.loadingDashboard': 'Carregando dashboard...',
+ 'rpt.complianceScore': 'Score de Compliance',
+ 'rpt.compliant': 'Conforme',
+ 'rpt.nonCompliant': 'Nao Conforme',
+ 'rpt.ofTotal': 'do total',
+ 'rpt.compliantItems': 'itens conformes',
+ 'rpt.totalFindings': 'achados totais',
+ 'rpt.evaluated': 'Avaliados',
+ 'rpt.controlsVerified': 'Controles verificados',
+ 'rpt.regionsCount': 'regiao(oes)',
+ 'rpt.complianceDist': 'Distribuicao de Compliance',
+ 'rpt.findingsBySection': 'Achados por Secao',
+ 'rpt.generatedAt': 'Gerado em',
+ 'rpt.htmlReport': 'Relatorio HTML',
+ 'rpt.files': 'Arquivos',
+ 'rpt.inSections': 'secoes',
+ 'rpt.fullEmbedding': 'Embedding Completo',
+ 'rpt.noActiveTables': 'Sem tabelas ativas',
+ 'rpt.executionHistory': 'Historico de Execucoes',
+ 'rpt.reports': 'relatorio(s)',
+ 'rpt.loading': 'Carregando...',
+ 'rpt.firstScanHint': 'Execute o primeiro scan CIS acima.',
+ 'rpt.selected': 'Selecionado',
+ 'rpt.deleteConfirm': 'Excluir este relatorio?',
+ 'rpt.deleteFailed': 'Falha ao excluir',
+ 'rpt.selectOciCredError': 'Selecione uma credencial OCI',
+ 'rpt.alreadyRunning': 'Ja existe um scan em execucao',
+ 'rpt.errorLoadReports': 'Erro ao carregar relatorios',
+ 'rpt.errorStartScan': 'Erro ao iniciar scan',
+ 'rpt.selectAdb': 'Selecione uma conexao ADB.',
+ 'rpt.selectTable': 'Selecione uma tabela.',
+ 'rpt.errorEmbedding': 'Erro ao gerar embedding',
+ 'rpt.cancelled': 'Cancelado',
+ 'rpt.completedAt': 'Concluido:',
+
+ // ββ Downloads ββ
+ 'dl.title': 'Downloads',
+ 'dl.subtitle': 'Baixe relatorios CIS gerados',
+ 'dl.allTenancies': 'Todas as Tenancies',
+ 'dl.allSections': 'Todas as secoes',
+ 'dl.reports': 'relatorio(s)',
+ 'dl.refresh': 'Atualizar',
+ 'dl.loadError': 'Erro ao carregar relatorios',
+ 'dl.loading': 'Carregando relatorios...',
+ 'dl.noReports': 'Nenhum relatorio concluido.',
+ 'dl.noReportsHint': 'Execute um scan na aba Reports para gerar relatorios.',
+ 'dl.files': 'arquivo(s)',
+ 'dl.hide': 'Ocultar',
+ 'dl.viewFiles': 'Ver Arquivos',
+ 'dl.loadingFiles': 'Carregando arquivos...',
+ 'dl.noFiles': 'Nenhum arquivo encontrado',
+ 'dl.filesInSections': 'arquivo(s) em {0} secoes',
+
+ // ββ OCI Config ββ
+ 'oci.title': 'Credenciais OCI',
+ 'oci.subtitle': 'Gerencie credenciais Oracle Cloud Infrastructure',
+ 'oci.registered': 'Credenciais Registradas',
+ 'oci.credential': 'credencial',
+ 'oci.credentials': 'credenciais',
+ 'oci.noCredentials': 'Nenhuma credencial registrada.',
+ 'oci.edit': 'Editar',
+ 'oci.test': 'Testar',
+ 'oci.delete': 'Excluir',
+ 'oci.yes': 'Sim',
+ 'oci.no': 'Nao',
+ 'oci.newCredential': 'Nova Credencial',
+ 'oci.editCredential': 'Editar Credencial',
+ 'oci.addDesc': 'Adicione as credenciais da sua conta OCI para executar reports e acessar servicos.',
+ 'oci.editing': 'Editando:',
+ 'oci.tenancyName': 'Tenancy Name',
+ 'oci.ocidTenancy': 'OCID Tenancy',
+ 'oci.region': 'Region',
+ 'oci.selectRegion': 'Selecione a regiao...',
+ 'oci.searchRegion': 'Buscar regiao...',
+ 'oci.noRegion': 'Nenhuma regiao encontrada',
+ 'oci.compartment': 'Compartment OCID',
+ 'oci.ocidUser': 'OCID User',
+ 'oci.fingerprint': 'Fingerprint',
+ 'oci.sessionToken': 'Session Token',
+ 'oci.sessionTokenHint': 'Cole o conteudo do arquivo token gerado por',
+ 'oci.sessionKeyLabel': 'Chave de Sessao (.pem)',
+ 'oci.privateKey': 'Private Key (.pem)',
+ 'oci.publicKey': 'Public Key (.pem)',
+ 'oci.keyPassphrase': 'Key Passphrase',
+ 'oci.keyPassphraseHint': 'Apenas se a chave privada for protegida por senha',
+ 'oci.sshPublicKey': 'SSH Public Key',
+ 'oci.sshPublicKeyHint': 'Chave SSH para acesso a instancias compute (ex: ssh-rsa AAAA...)',
+ 'oci.saveCredential': 'Salvar Credencial',
+ 'oci.saveChanges': 'Salvar Alteracoes',
+ 'oci.cancel': 'Cancelar',
+ 'oci.saving': 'Salvando credencial...',
+ 'oci.saved': 'Credencial salva com sucesso!',
+ 'oci.updated': 'Credencial atualizada com sucesso!',
+ 'oci.deleting': 'Excluindo credencial...',
+ 'oci.deleted': 'Credencial excluida com sucesso!',
+ 'oci.fillField': 'Preencha o',
+ 'oci.fillOcidTenancy': 'Preencha o OCID Tenancy',
+ 'oci.fillUserAndFP': 'Preencha OCID User e Fingerprint',
+ 'oci.pasteToken': 'Cole o Session Token',
+ 'oci.selectKey': 'Selecione a chave privada (.pem)',
+ 'oci.selectSessionKey': 'Selecione a chave de sessao (.pem)',
+ 'oci.changeHint': 'Preencha para alterar. Atual:',
+ 'oci.keepKey': 'Deixe vazio para manter a chave atual',
+ 'oci.typeCannotChange': '(tipo nao pode ser alterado)',
+ 'oci.sshNotConfigured': 'nao configurada',
+
+ // ββ GenAI Config ββ
+ 'genai.title': 'Modelos GenAI',
+ 'genai.subtitle': 'Configure modelos de IA generativa para o Chat Agent',
+ 'genai.configured': 'Modelos Configurados',
+ 'genai.model': 'modelo',
+ 'genai.models': 'modelos',
+ 'genai.noModels': 'Nenhum modelo configurado.',
+ 'genai.newModel': 'Novo Modelo GenAI',
+ 'genai.editModel': 'Editar Modelo',
+ 'genai.configName': 'Nome da Config',
+ 'genai.ociCredential': 'Credencial OCI',
+ 'genai.modelSelect': 'Modelo',
+ 'genai.genaiRegion': 'Regiao GenAI',
+ 'genai.compartment': 'Compartment OCID',
+ 'genai.modelOcid': 'Model OCID',
+ 'genai.modelOcidHint': 'Cole o OCID do modelo do portal OCI (opcional)',
+ 'genai.customOcid': 'Personalizado (usar OCID)',
+ 'genai.default': 'Modelo Padrao',
+ 'genai.setDefault': 'Definir como Padrao',
+ 'genai.saveModel': 'Salvar Modelo',
+ 'genai.saveChanges': 'Salvar Alteracoes',
+ 'genai.cancel': 'Cancelar',
+ 'genai.saving': 'Salvando modelo...',
+ 'genai.deleting': 'Excluindo modelo...',
+ 'genai.addDesc': 'Adicione modelos personalizados ou crie presets com credenciais especificas.',
+ 'genai.fillName': 'Preencha o nome da config',
+ 'genai.selectOci': 'Selecione uma credencial OCI',
+ 'genai.fillOcid': 'Informe o Model OCID para modelo personalizado',
+
+ // ββ MCP Servers ββ
+ 'mcp.title': 'MCP Servers',
+ 'mcp.subtitle': 'Servidores MCP para integracao com ferramentas e execucao de tarefas',
+ 'mcp.registered': 'Servidores Registrados',
+ 'mcp.noServers': 'Nenhum MCP server registrado.',
+ 'mcp.edit': 'Editar',
+ 'mcp.activate': 'Ativar',
+ 'mcp.deactivate': 'Desativar',
+ 'mcp.active': 'Ativo',
+ 'mcp.inactive': 'Inativo',
+ 'mcp.delete': 'Excluir',
+ 'mcp.yes': 'Sim',
+ 'mcp.no': 'Nao',
+ 'mcp.command': 'Comando:',
+ 'mcp.url': 'URL:',
+ 'mcp.module': 'Modulo:',
+ 'mcp.adb': 'ADB:',
+ 'mcp.noAdb': 'Nenhum',
+ 'mcp.tools': 'Tools',
+ 'mcp.noTools': 'Nenhuma tool registrada',
+ 'mcp.discoverTools': 'Descobrir Tools',
+ 'mcp.discovering': 'Conectando ao MCP server para descobrir tools...',
+ 'mcp.newServer': 'Registrar Novo Server',
+ 'mcp.editServer': 'Editar Server',
+ 'mcp.name': 'Nome',
+ 'mcp.description': 'Descricao',
+ 'mcp.serverType': 'Tipo de Servidor',
+ 'mcp.commandLabel': 'Comando',
+ 'mcp.commandHint': 'Comando para iniciar o servidor',
+ 'mcp.argsLabel': 'Argumentos (JSON)',
+ 'mcp.argsHint': 'Array de argumentos passados ao comando',
+ 'mcp.urlLabel': 'URL do Servidor',
+ 'mcp.urlHint': 'Endpoint SSE do servidor MCP',
+ 'mcp.linkAdb': 'Vincular ADB Vector',
+ 'mcp.linkAdbHint': 'Opcional β o MCP server podera usar este banco como ferramenta.',
+ 'mcp.saveServer': 'Registrar Server',
+ 'mcp.saveChanges': 'Salvar Alteracoes',
+ 'mcp.cancel': 'Cancelar',
+ 'mcp.saving': 'Registrando servidor...',
+ 'mcp.updating': 'Atualizando servidor...',
+ 'mcp.fillName': 'Preencha o nome do server',
+ 'mcp.invalidArgs': 'Argumentos deve ser um JSON array valido',
+ 'mcp.configDesc': 'Configure um novo servidor MCP. Os campos se ajustam ao tipo selecionado.',
+
+ // ββ ADB Config ββ
+ 'adb.title': 'ADB Connections',
+ 'adb.subtitle': 'Conexoes com Oracle Autonomous Database via Wallet (mTLS)',
+ 'adb.registered': 'Conexoes Registradas',
+ 'adb.connection': 'conexao',
+ 'adb.connections': 'conexoes',
+ 'adb.noConnections': 'Nenhuma conexao ADB registrada.',
+ 'adb.edit': 'Editar',
+ 'adb.test': 'Testar',
+ 'adb.delete': 'Excluir',
+ 'adb.yes': 'Sim',
+ 'adb.no': 'Nao',
+ 'adb.newConnection': 'Nova Conexao',
+ 'adb.editConnection': 'Editar Conexao',
+ 'adb.connName': 'Nome da Conexao',
+ 'adb.walletZip': 'Wallet ZIP',
+ 'adb.walletHint': 'Envie o wallet para extrair os DSNs do tnsnames.ora',
+ 'adb.walletEditHint': 'Envie novo wallet ou deixe vazio para manter o atual',
+ 'adb.analyze': 'Analisar',
+ 'adb.dsn': 'DSN (TNS Name)',
+ 'adb.dsnHint': 'Selecione ou digite o DSN. Ex: myatp_high',
+ 'adb.username': 'Username',
+ 'adb.password': 'Password',
+ 'adb.walletPassword': 'Wallet Password',
+ 'adb.genaiConfig': 'Config GenAI (Embeddings)',
+ 'adb.genaiConfigHint': 'Credenciais OCI usadas para gerar embeddings via GenAI.',
+ 'adb.noRag': 'Nenhum (sem RAG)',
+ 'adb.embeddingModel': 'Modelo de Embedding',
+ 'adb.embeddingModelHint': 'Modelo OCI GenAI para gerar vetores.',
+ 'adb.saveConnection': 'Salvar Conexao',
+ 'adb.saveChanges': 'Salvar Alteracoes',
+ 'adb.cancel': 'Cancelar',
+ 'adb.saving': 'Salvando conexao...',
+ 'adb.deleting': 'Excluindo conexao...',
+ 'adb.updateWallet': 'Atualizar Wallet',
+ 'adb.adbConnection': 'Conexao ADB',
+ 'adb.uploadWallet': 'Upload Wallet',
+ 'adb.connDesc': 'Conexao via python-oracledb Thin mode com Wallet (mTLS) para Oracle Autonomous Database.',
+ 'adb.selectWallet': 'Selecione um arquivo wallet ZIP.',
+ 'adb.selectConnection': 'Selecione uma conexao ADB.',
+ 'adb.fillName': 'Preencha o nome da conexao',
+ 'adb.fillDsn': 'Preencha o DSN',
+ 'adb.fillUsername': 'Preencha o username',
+ 'adb.fillPassword': 'Preencha a senha',
+
+ // ββ Embeddings ββ
+ 'emb.title': 'Embeddings',
+ 'emb.subtitle': 'Base de conhecimento vetorial β upload, importacao e gerenciamento',
+ 'emb.noAdb': 'Nenhuma conexao ADB configurada.',
+ 'emb.noAdbHint': 'Configure em ADB Vector.',
+ 'emb.noOci': 'Nenhuma credencial OCI configurada.',
+ 'emb.noOciHint': 'Configure em Config β OCI.',
+ 'emb.existing': 'Embeddings Existentes',
+ 'emb.existingDesc': 'Consulte os documentos armazenados nas tabelas de embeddings do ADB.',
+ 'emb.adbConnection': 'Conexao ADB',
+ 'emb.table': 'Tabela',
+ 'emb.load': 'Carregar',
+ 'emb.noEmbeddings': 'Nenhum embedding encontrado.',
+ 'emb.total': 'Total: {0} documentos',
+ 'emb.cisTitle': 'CIS Recommendations',
+ 'emb.cisDesc': 'Envie a versao mais recente do PDF para manter as recomendacoes CIS Oracle Cloud atualizadas na base vetorial.',
+ 'emb.pdfFile': 'Arquivo PDF',
+ 'emb.uploadCis': 'Upload CIS Recommendations',
+ 'emb.kbTitle': 'Base de Conhecimento',
+ 'emb.kbDesc': 'Envie documentos para alimentar a base de conhecimento da equipe. Os arquivos serao vetorizados e disponibilizados para consulta via chat.',
+ 'emb.fileLabel': 'Arquivo (.txt, .pdf, .csv, .json, .md)',
+ 'emb.uploadFile': 'Upload Arquivo',
+ 'emb.urlLabel': 'URL (pagina web ou PDF)',
+ 'emb.importUrl': 'Importar URL',
+ 'emb.purgeTitle': 'Purge & Re-embed',
+ 'emb.purgeDesc': 'Limpe os embeddings de uma tabela para liberar espaco ou preparar para re-ingestao. Esta acao e irreversivel.',
+ 'emb.tenancyOptional': 'Tenancy (opcional)',
+ 'emb.purge': 'Purge',
+ 'emb.confirm': 'Confirmar',
+ 'emb.selectTable': 'Selecione uma tabela.',
+ 'emb.selectAdbAndFile': 'Selecione conexao ADB e arquivo.',
+ 'emb.selectAdbAndUrl': 'Selecione conexao ADB e informe a URL.',
+ 'emb.processing': 'Processando PDF e gerando embeddings...',
+ 'emb.uploading': 'Enviando e gerando embeddings...',
+ 'emb.fetchingUrl': 'Buscando conteudo e gerando embeddings...',
+ 'emb.purging': 'Limpando embeddings...',
+
+ // ββ EmbConsult ββ
+ 'ec.title': 'Consultar Embeddings',
+ 'ec.subtitle': 'Pergunte sobre os dados armazenados nas bases vetoriais',
+ 'ec.clear': 'Limpar',
+ 'ec.tableOptional': 'Tabela (opcional)',
+ 'ec.allTables': 'Todas as tabelas',
+ 'ec.emptyTitle': 'Faca uma consulta para pesquisar nos embeddings.',
+ 'ec.emptyHint': 'Ex: "findings para CIS 1.1", "recomendacoes de networking", "como corrigir MFA"',
+ 'ec.searching': 'Consultando bases vetoriais...',
+ 'ec.placeholder': 'Digite sua consulta...',
+ 'ec.submit': 'Consultar',
+ 'ec.sources': 'Fontes consultadas',
+ 'ec.noAnswer': 'Sem resposta do modelo.',
+
+ // ββ Users ββ
+ 'usr.title': 'Usuarios',
+ 'usr.activeCount': 'ativo',
+ 'usr.activesCount': 'ativos',
+ 'usr.inactiveCount': 'inativo',
+ 'usr.inactivesCount': 'inativos',
+ 'usr.total': 'total',
+ 'usr.newUser': 'Novo Usuario',
+ 'usr.cancel': 'Cancelar',
+ 'usr.createUser': 'Criar Novo Usuario',
+ 'usr.firstName': 'Nome',
+ 'usr.lastName': 'Sobrenome',
+ 'usr.username': 'Usuario (login)',
+ 'usr.email': 'Email',
+ 'usr.password': 'Senha',
+ 'usr.role': 'Role',
+ 'usr.creating': 'Criando...',
+ 'usr.create': 'Criar Usuario',
+ 'usr.noUsers': 'Nenhum usuario cadastrado',
+ 'usr.noUsersHint': 'Clique em "Novo Usuario" para criar o primeiro usuario.',
+ 'usr.name': 'Nome',
+ 'usr.mfa': 'MFA',
+ 'usr.status': 'Status',
+ 'usr.lastLogin': 'Ultimo Login',
+ 'usr.actions': 'Acoes',
+ 'usr.active': 'Ativo',
+ 'usr.inactive': 'Inativo',
+ 'usr.never': 'Nunca',
+ 'usr.confirm': 'Confirmar',
+ 'usr.deactivate': 'Desativar usuario',
+ 'usr.created': 'Usuario criado com sucesso!',
+ 'usr.roleUpdated': 'Role atualizada',
+ 'usr.deactivated': 'Usuario desativado',
+ 'usr.requiredName': 'Nome e sobrenome sao obrigatorios',
+ 'usr.requiredUsername': 'Usuario (login) e obrigatorio',
+ 'usr.requiredPassword': 'Senha e obrigatoria',
+
+ // ββ MFA ββ
+ 'mfa.title': 'Autenticacao MFA (TOTP)',
+ 'mfa.subtitle': 'Proteja sua conta com autenticacao em dois fatores via Google Authenticator ou Authy.',
+ 'mfa.enabled': 'MFA esta ativado',
+ 'mfa.disabled': 'MFA esta desativado',
+ 'mfa.enabledDesc': 'Sua conta esta protegida com autenticacao de dois fatores.',
+ 'mfa.disabledDesc': 'Ative o MFA para adicionar uma camada extra de seguranca.',
+ 'mfa.disableHint': 'Para desativar o MFA, clique no botao abaixo. Isso reduzira a seguranca da sua conta.',
+ 'mfa.disableBtn': 'Desativar MFA',
+ 'mfa.confirmDisable': 'Deseja realmente desativar o MFA da sua conta?',
+ 'mfa.scanQr': 'Escaneie o QR Code ou copie o secret',
+ 'mfa.openApp': 'Abra seu app autenticador (Google Authenticator, Authy, etc.)',
+ 'mfa.enterCode': 'Insira o codigo gerado pelo app',
+ 'mfa.activateMfa': 'Ativar MFA',
+ 'mfa.generateSecret': 'Gerar Secret',
+ 'mfa.generateHint': 'Clique no botao abaixo para gerar um secret TOTP. Voce precisara de um app autenticador para escanear o QR Code.',
+ 'mfa.activated': 'MFA ativado com sucesso!',
+ 'mfa.deactivated': 'MFA desativado.',
+ 'mfa.invalidCode': 'Informe um codigo de 6 digitos',
+
+ // ββ Audit ββ
+ 'audit.title': 'Log de Auditoria',
+ 'audit.subtitle': 'Monitoramento de atividades do sistema, configuracoes e sessoes de chat.',
+ 'audit.tabAudit': 'Audit Log',
+ 'audit.tabConfig': 'Config Logs',
+ 'audit.tabChat': 'Chat Logs',
+ 'audit.records': 'registros',
+ 'audit.refresh': 'Atualizar',
+ 'audit.all': 'Todos',
+ 'audit.errors': 'Erros',
+ 'audit.success': 'Sucesso',
+ 'audit.info': 'Info',
+ 'audit.loadingLogs': 'Carregando logs...',
+ 'audit.noAudit': 'Nenhum registro de auditoria.',
+ 'audit.noConfig': 'Nenhum log de configuracao registrado.',
+ 'audit.noChat': 'Nenhum log de chat registrado.',
+ 'audit.date': 'Data',
+ 'audit.user': 'Usuario',
+ 'audit.action': 'Acao',
+ 'audit.resource': 'Recurso',
+ 'audit.details': 'Detalhes',
+ 'audit.ip': 'IP',
+ 'audit.config': 'Config',
+ 'audit.status': 'Status',
+ 'audit.message': 'Mensagem',
+ 'audit.session': 'Sessao',
+ 'audit.source': 'Origem',
+
+ // ββ Common ββ
+ 'common.save': 'Salvar',
+ 'common.cancel': 'Cancelar',
+ 'common.delete': 'Excluir',
+ 'common.edit': 'Editar',
+ 'common.create': 'Criar',
+ 'common.update': 'Atualizar',
+ 'common.close': 'Fechar',
+ 'common.confirm': 'Confirmar',
+ 'common.back': 'Voltar',
+ 'common.search': 'Buscar',
+ 'common.refresh': 'Atualizar',
+ 'common.download': 'Download',
+ 'common.upload': 'Upload',
+ 'common.copy': 'Copiar',
+ 'common.retry': 'Tentar novamente',
+ 'common.send': 'Enviar',
+ 'common.clear': 'Limpar',
+ 'common.test': 'Testar',
+ 'common.yes': 'Sim',
+ 'common.no': 'Nao',
+ 'common.loading': 'Carregando...',
+ 'common.saving': 'Salvando...',
+ 'common.select': 'Selecione...',
+ 'common.noData': 'Nenhum dado encontrado',
+ 'common.error': 'Erro',
+ 'common.success': 'Sucesso',
+};
+
+export default pt;
diff --git a/frontend-react/src/index.css b/frontend-react/src/index.css
new file mode 100644
index 0000000..7019aa0
--- /dev/null
+++ b/frontend-react/src/index.css
@@ -0,0 +1,437 @@
+@import 'tailwindcss/theme' layer(theme);
+@import 'tailwindcss/preflight' layer(base);
+@import 'tailwindcss/utilities' layer(utilities);
+
+/* ββ Oracle Dark Premium Theme (in theme layer) ββ */
+@layer theme {
+ :root, html.light {
+ --bg: #f4f2ef; --bg1: #ffffff; --bg2: #faf9f7; --bg3: #efeee9; --bg4: #e5e3dd;
+ --bd: #ddd9d2; --bdf: #c5c0b8;
+ --t1: #1c1917; --t2: #44403c; --t3: #78716c; --t4: #a8a29e;
+ --ac: #c74634; --ach: #b13d2e; --acd: #9a3427; --acl: rgba(199,70,52,.06); --acl2: rgba(199,70,52,.1);
+ --gn: #16a34a; --gnl: rgba(22,163,74,.08); --rd: #dc2626; --rdl: rgba(220,38,38,.06);
+ --bl: #2563eb; --bll: rgba(37,99,235,.06); --yl: #ca8a04; --yll: rgba(202,138,4,.06);
+ --pp: #7c3aed; --ppl: rgba(124,58,237,.06);
+ --ok: #16a34a; --w: #ca8a04; --err: #dc2626;
+ --r: 12px; --rl: 16px; --rr: 20px;
+ --sh1: 0 1px 2px rgba(28,25,23,.04), 0 1px 3px rgba(28,25,23,.06);
+ --sh2: 0 4px 6px -1px rgba(28,25,23,.05), 0 2px 4px -2px rgba(28,25,23,.04);
+ --sh3: 0 10px 15px -3px rgba(28,25,23,.06), 0 4px 6px -4px rgba(28,25,23,.04);
+ --shh: 0 20px 25px -5px rgba(28,25,23,.08), 0 8px 10px -6px rgba(28,25,23,.04);
+ --tb-bg: rgba(255,255,255,.8);
+ --fm: 'JetBrains Mono', monospace;
+ --fs: 'Plus Jakarta Sans', -apple-system, system-ui, sans-serif;
+ --trans: cubic-bezier(.22,1,.36,1);
+ }
+
+ html.dark {
+ --bg: #0d0f14; --bg1: #151820; --bg2: #1a1e28; --bg3: #222733; --bg4: #2a3040;
+ --bd: #2a3040; --bdf: #3a4258;
+ --t1: #f0f0f4; --t2: #c8cad0; --t3: #8b8fa3; --t4: #5a5e6e;
+ --ac: #c74634; --ach: #d9523f; --acd: #a83428; --acl: rgba(199,70,52,.1); --acl2: rgba(199,70,52,.16);
+ --gn: #34c759; --gnl: rgba(52,199,89,.1); --rd: #ff453a; --rdl: rgba(255,69,58,.1);
+ --bl: #0a84ff; --bll: rgba(10,132,255,.1); --yl: #ffd60a; --yll: rgba(255,214,10,.1);
+ --pp: #bf5af2; --ppl: rgba(191,90,242,.1);
+ --ok: #34c759; --w: #ffd60a; --err: #ff453a;
+ --sh1: 0 1px 3px rgba(0,0,0,.3), 0 1px 2px rgba(0,0,0,.2);
+ --sh2: 0 4px 8px rgba(0,0,0,.3), 0 2px 4px rgba(0,0,0,.2);
+ --sh3: 0 10px 20px rgba(0,0,0,.35), 0 4px 8px rgba(0,0,0,.2);
+ --shh: 0 20px 30px rgba(0,0,0,.4), 0 8px 12px rgba(0,0,0,.25);
+ --tb-bg: rgba(21,24,32,.85);
+ }
+}
+
+/* ββ Base resets (in base layer) ββ */
+@layer base {
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
+ body {
+ font-family: var(--fs);
+ background: var(--bg);
+ color: var(--t1);
+ height: 100vh;
+ overflow: hidden;
+ -webkit-font-smoothing: antialiased;
+ }
+ ::selection { background: rgba(199,70,52,.2); color: var(--t1); }
+
+ html.light *, html.dark * {
+ transition: background-color .25s var(--trans), border-color .25s var(--trans), color .15s var(--trans);
+ }
+
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
+ ::-webkit-scrollbar-track { background: transparent; }
+ ::-webkit-scrollbar-thumb { background: var(--bd); border-radius: 3px; }
+ ::-webkit-scrollbar-thumb:hover { background: var(--bdf); }
+
+ @keyframes fadeIn {
+ from { opacity: 0; transform: translateY(12px); }
+ to { opacity: 1; transform: translateY(0); }
+ }
+
+ /* Form elements */
+ select {
+ appearance: none;
+ background: var(--bg1) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2378716c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E") no-repeat right 12px center;
+ border: 1px solid var(--bd);
+ border-radius: var(--r);
+ padding: 9px 36px 9px 14px;
+ font-size: .82rem;
+ font-family: var(--fs);
+ color: var(--t1);
+ cursor: pointer;
+ transition: border-color .2s, box-shadow .2s;
+ min-width: 0;
+ }
+ select:focus {
+ outline: none;
+ border-color: var(--ac);
+ box-shadow: 0 0 0 3px var(--acl);
+ }
+ select:hover { border-color: var(--bdf); }
+
+ input[type="text"], input[type="email"], input[type="password"], input[type="number"], input[type="search"],
+ textarea {
+ background: var(--bg1);
+ border: 1px solid var(--bd);
+ border-radius: var(--r);
+ padding: 9px 14px;
+ font-size: .82rem;
+ font-family: var(--fs);
+ color: var(--t1);
+ transition: border-color .2s, box-shadow .2s;
+ width: 100%;
+ }
+ input[type="text"]:focus, input[type="email"]:focus, input[type="password"]:focus,
+ input[type="number"]:focus, input[type="search"]:focus, textarea:focus {
+ outline: none;
+ border-color: var(--ac);
+ box-shadow: 0 0 0 3px var(--acl);
+ }
+
+ input[type="checkbox"] {
+ appearance: none;
+ width: 18px;
+ height: 18px;
+ border: 2px solid var(--bd);
+ border-radius: 5px;
+ background: var(--bg1);
+ cursor: pointer;
+ transition: all .15s;
+ flex-shrink: 0;
+ position: relative;
+ }
+ input[type="checkbox"]:checked {
+ background: var(--ac);
+ border-color: var(--ac);
+ }
+ input[type="checkbox"]:checked::after {
+ content: '';
+ position: absolute;
+ left: 4px;
+ top: 1px;
+ width: 6px;
+ height: 10px;
+ border: solid white;
+ border-width: 0 2px 2px 0;
+ transform: rotate(45deg);
+ }
+}
+
+/* ββ Component styles β OUTSIDE layers so they override Tailwind utilities ββ */
+
+/* Nav item hover */
+nav a:hover { background: var(--bg3); color: var(--t1) !important; }
+
+/* Page wrapper */
+.page {
+ padding: 28px 32px;
+ max-width: 1280px;
+ animation: fadeIn .35s ease;
+}
+.page-full {
+ height: 100%;
+ animation: fadeIn .35s ease;
+}
+
+/* Page header */
+.page-header {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ margin-bottom: 24px;
+}
+.page-header h1 {
+ font-size: 1.35rem;
+ font-weight: 700;
+ color: var(--t1);
+ letter-spacing: -.01em;
+}
+.page-header .subtitle {
+ font-size: .8rem;
+ color: var(--t3);
+ margin-top: 2px;
+}
+.page-header .spacer { flex: 1; }
+.page-header .count {
+ font-size: .75rem;
+ font-weight: 600;
+ color: var(--t3);
+ background: var(--bg3);
+ padding: 4px 12px;
+ border-radius: 20px;
+}
+
+/* Cards */
+.card {
+ background: var(--bg1);
+ border: 1px solid var(--bd);
+ border-radius: var(--rl);
+ box-shadow: var(--sh1);
+ padding: 20px 24px;
+ margin-bottom: 16px;
+}
+.card-header {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 16px;
+ padding-bottom: 12px;
+ border-bottom: 1px solid var(--bd);
+}
+.card-header h2 {
+ font-size: .92rem;
+ font-weight: 700;
+ color: var(--t1);
+}
+.card-header .icon {
+ width: 36px;
+ height: 36px;
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+/* Tables */
+.data-table {
+ width: 100%;
+ border-collapse: separate;
+ border-spacing: 0;
+ font-size: .82rem;
+}
+.data-table thead th {
+ text-align: left;
+ font-size: .7rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: .06em;
+ color: var(--t3);
+ padding: 10px 14px;
+ background: var(--bg2);
+ border-bottom: 1px solid var(--bd);
+}
+.data-table thead th:first-child { border-radius: var(--r) 0 0 0; }
+.data-table thead th:last-child { border-radius: 0 var(--r) 0 0; }
+.data-table tbody td {
+ padding: 12px 14px;
+ color: var(--t2);
+ border-bottom: 1px solid var(--bg3);
+ vertical-align: middle;
+}
+.data-table tbody tr:last-child td { border-bottom: none; }
+.data-table tbody tr:hover td { background: var(--bg2); }
+
+/* Buttons */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 9px 18px;
+ border-radius: var(--r);
+ font-size: .82rem;
+ font-weight: 600;
+ font-family: var(--fs);
+ cursor: pointer;
+ border: 1px solid transparent;
+ transition: all .2s var(--trans);
+ white-space: nowrap;
+}
+.btn:disabled {
+ opacity: .5;
+ cursor: not-allowed;
+}
+.btn-primary {
+ background: var(--ac);
+ color: #fff;
+ border-color: var(--ac);
+}
+.btn-primary:hover:not(:disabled) { background: var(--ach); }
+.btn-secondary {
+ background: var(--bg1);
+ color: var(--t2);
+ border-color: var(--bd);
+}
+.btn-secondary:hover:not(:disabled) { background: var(--bg3); color: var(--t1); }
+.btn-danger {
+ background: transparent;
+ color: var(--rd);
+ border-color: var(--rd);
+}
+.btn-danger:hover:not(:disabled) { background: var(--rdl); }
+.btn-success {
+ background: var(--gn);
+ color: #fff;
+ border-color: var(--gn);
+}
+.btn-sm {
+ padding: 6px 12px;
+ font-size: .75rem;
+ border-radius: 8px;
+}
+
+/* Badges */
+.badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 3px 10px;
+ border-radius: 20px;
+ font-size: .68rem;
+ font-weight: 600;
+ white-space: nowrap;
+}
+.badge-green { background: var(--gnl); color: var(--gn); }
+.badge-red { background: var(--rdl); color: var(--rd); }
+.badge-blue { background: var(--bll); color: var(--bl); }
+.badge-yellow { background: var(--yll); color: var(--yl); }
+.badge-purple { background: var(--ppl); color: var(--pp); }
+.badge-gray { background: var(--bg3); color: var(--t3); }
+
+/* Empty state */
+.empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 60px 20px;
+ text-align: center;
+ color: var(--t4);
+}
+.empty-state svg { opacity: .35; margin-bottom: 12px; }
+.empty-state h3 { font-size: .92rem; font-weight: 600; color: var(--t3); margin-bottom: 4px; }
+.empty-state p { font-size: .78rem; }
+
+/* KPI cards */
+.kpi-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: 16px;
+ margin-bottom: 24px;
+}
+.kpi-card {
+ background: var(--bg1);
+ border: 1px solid var(--bd);
+ border-radius: var(--rl);
+ padding: 18px 20px;
+ box-shadow: var(--sh1);
+}
+.kpi-card .label {
+ font-size: .7rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: .06em;
+ color: var(--t3);
+ margin-bottom: 8px;
+}
+.kpi-card .value {
+ font-size: 1.5rem;
+ font-weight: 800;
+ color: var(--t1);
+ letter-spacing: -.02em;
+}
+.kpi-card .sub {
+ font-size: .72rem;
+ color: var(--t4);
+ margin-top: 4px;
+}
+
+/* Form groups */
+.form-group {
+ margin-bottom: 16px;
+}
+.form-group label {
+ display: block;
+ font-size: .75rem;
+ font-weight: 600;
+ color: var(--t2);
+ margin-bottom: 6px;
+}
+.form-row {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 16px;
+}
+
+/* Tags/pills */
+.tag {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 4px 10px;
+ border-radius: 20px;
+ font-size: .72rem;
+ font-weight: 500;
+ background: var(--bg3);
+ color: var(--t2);
+ border: 1px solid var(--bd);
+}
+.tag button {
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: var(--t3);
+ padding: 0;
+ font-size: .82rem;
+ line-height: 1;
+}
+.tag button:hover { color: var(--rd); }
+
+/* Section divider */
+.section-title {
+ font-size: .72rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: .08em;
+ color: var(--t4);
+ margin: 24px 0 12px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid var(--bg3);
+}
+
+/* Prevent text overflow on resize */
+.prose pre, .chat-markdown pre {
+ overflow-x: auto;
+ max-width: 100%;
+}
+.prose table, .chat-markdown table {
+ overflow-x: auto;
+ display: block;
+ max-width: 100%;
+}
+.prose code, .chat-markdown code {
+ word-break: break-all;
+}
+.prose p, .chat-markdown p {
+ overflow-wrap: break-word;
+ word-break: break-word;
+}
+
+/* Tooltip-like labels */
+.field-label {
+ font-size: .7rem;
+ font-weight: 600;
+ color: var(--t3);
+ text-transform: uppercase;
+ letter-spacing: .04em;
+}
diff --git a/frontend-react/src/main.tsx b/frontend-react/src/main.tsx
new file mode 100644
index 0000000..6c1e1f3
--- /dev/null
+++ b/frontend-react/src/main.tsx
@@ -0,0 +1,13 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+import './index.css';
+import App from './App.tsx';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+
+ ,
+);
diff --git a/frontend-react/src/pages/ChatPage.tsx b/frontend-react/src/pages/ChatPage.tsx
new file mode 100644
index 0000000..01b62e0
--- /dev/null
+++ b/frontend-react/src/pages/ChatPage.tsx
@@ -0,0 +1,1138 @@
+import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
+import {
+ Menu, Settings, Plus, Trash2, Send, Paperclip, X, RefreshCw,
+ Bot, ChevronDown, Database, Wrench, Loader2, Search,
+} from 'lucide-react';
+import ReactMarkdown from 'react-markdown';
+import remarkGfm from 'remark-gfm';
+import { useAppStore } from '@/stores/app';
+import { useI18n } from '@/i18n';
+import {
+ chatApi,
+ type ChatSession,
+ type ChatSendBody,
+} from '@/api/endpoints/chat';
+
+/* ββ Local types ββ */
+
+interface UIMessage {
+ role: 'user' | 'assistant';
+ content: string;
+ timestamp: string;
+ failed?: boolean;
+ thinking?: boolean;
+}
+
+interface AttachedFile {
+ file: File;
+ name: string;
+ type: string;
+ preview?: string;
+}
+
+interface ChatParams {
+ temperature: number;
+ max_tokens: number;
+ top_p: number;
+ top_k: number;
+ frequency_penalty: number;
+ presence_penalty: number;
+ reasoning_effort: string;
+}
+
+const ACCEPT =
+ 'image/*,.pdf,.txt,.csv,.json,.log,.xml,.yaml,.yml,.md,.py,.js,.ts,.html,.css,.sql,.tf,.hcl';
+
+const DEFAULT_PARAMS: ChatParams = {
+ temperature: 1,
+ max_tokens: 6000,
+ top_p: 0.95,
+ top_k: 1,
+ frequency_penalty: 0,
+ presence_penalty: 0,
+ reasoning_effort: 'medium',
+};
+
+/* ββ Helpers ββ */
+
+function nowHHmm(): string {
+ return new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
+}
+
+function timeAgo(dateStr: string): string {
+ const tFn = useI18n.getState().t;
+ const d = new Date(dateStr.endsWith('Z') ? dateStr : dateStr + 'Z');
+ const diff = (Date.now() - d.getTime()) / 1000;
+ if (diff < 60) return tFn('chat.timeNow');
+ if (diff < 3600) return `${Math.floor(diff / 60)}min`;
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h`;
+ return `${Math.floor(diff / 86400)}d`;
+}
+
+function dateGroup(dateStr: string): string {
+ const tFn = useI18n.getState().t;
+ if (!dateStr) return tFn('chat.other');
+ const d = new Date(dateStr.endsWith('Z') ? dateStr : dateStr + 'Z');
+ const diff = (Date.now() - d.getTime()) / 86400000;
+ if (diff < 1) return tFn('chat.today');
+ if (diff < 2) return tFn('chat.yesterday');
+ if (diff < 7) return tFn('chat.last7');
+ if (diff < 30) return tFn('chat.last30');
+ return tFn('chat.older');
+}
+
+function groupSessions(sessions: ChatSession[]): Record {
+ const groups: Record = {};
+ for (const s of sessions) {
+ const g = dateGroup(s.updated_at || s.created_at);
+ (groups[g] ??= []).push(s);
+ }
+ return groups;
+}
+
+/* ββ Component ββ */
+
+export default function ChatPage() {
+ /* ββ Store ββ */
+ const { ociCfg, genaiCfg, models, mcpSvr, adbCfg } = useAppStore();
+ const { t } = useI18n();
+
+ /* ββ State ββ */
+ const [messages, setMessages] = useState([]);
+ const [sessionId, setSessionId] = useState(null);
+ const [sessions, setSessions] = useState([]);
+ const [model, setModel] = useState('');
+ const [ociConfig, setOciConfig] = useState('');
+ const [files, setFiles] = useState([]);
+ const [chatParams, setChatParams] = useState({ ...DEFAULT_PARAMS });
+ const [useTools, setUseTools] = useState(true);
+ const [historyOpen, setHistoryOpen] = useState(false);
+ const [settingsOpen, setSettingsOpen] = useState(false);
+ const [sending, setSending] = useState(false);
+ const [modelDropOpen, setModelDropOpen] = useState(false);
+ const [modelSearch, setModelSearch] = useState('');
+
+ const inputRef = useRef(null);
+ const messagesEndRef = useRef(null);
+ const fileInputRef = useRef(null);
+ const modelDropRef = useRef(null);
+ const pollRef = useRef | null>(null);
+
+ /* ββ Derived ββ */
+ const isDirect = !!model && !model.startsWith('cfg:');
+
+ const modelInfo = useMemo(() => {
+ let mid = '';
+ if (model.startsWith('cfg:')) {
+ const g = genaiCfg.find((x) => x.id === model.slice(4));
+ if (g) mid = g.model_id;
+ } else {
+ mid = model;
+ }
+ return models[mid] || ({} as { provider?: string; reasoning?: boolean; max_tokens?: number; penalties?: boolean });
+ }, [model, models, genaiCfg]);
+
+ const isReasoning = !!(modelInfo as any).reasoning;
+ const maxTokenLimit = (modelInfo as any).max_tokens || 128000;
+ const hasTopK = ['meta', 'google', ''].includes((modelInfo as any).provider || '');
+ const hasPenalties = !!(modelInfo as any).penalties;
+
+ const toolCount = useMemo(
+ () =>
+ mcpSvr
+ .filter((m) => m.is_active && Array.isArray(m.tools) && m.tools.length)
+ .reduce((a, m) => a + (m.tools?.length || 0), 0),
+ [mcpSvr],
+ );
+
+ const hasRag = useMemo(
+ () => adbCfg.some((c) => c.is_active) && (genaiCfg.length > 0 || ociCfg.length > 0),
+ [adbCfg, genaiCfg, ociCfg],
+ );
+
+ const modelLabel = useMemo(() => {
+ if (!model) return t('chat.selectModel');
+ if (model.startsWith('cfg:')) {
+ const g = genaiCfg.find((x) => x.id === model.slice(4));
+ if (!g) return 'Config...';
+ const mi = models[g.model_id];
+ return `${g.name || mi?.name || g.model_id} (${g.genai_region})`;
+ }
+ const mi = models[model];
+ return mi?.name || model;
+ }, [model, genaiCfg, models, t]);
+
+ /* Model dropdown items grouped by provider */
+ const modelGroups = useMemo(() => {
+ const groups: { label: string; items: { value: string; name: string }[] }[] = [];
+ const q = modelSearch.toLowerCase();
+
+ // GenAI configs
+ if (genaiCfg.length) {
+ const items = genaiCfg
+ .map((g) => {
+ const mi = models[g.model_id];
+ const name = `${g.name || mi?.name || g.model_id} (${g.genai_region})`;
+ return { value: `cfg:${g.id}`, name };
+ })
+ .filter((i) => !q || i.name.toLowerCase().includes(q));
+ if (items.length) groups.push({ label: t('chat.savedConfigs'), items });
+ }
+
+ // Direct models grouped by provider
+ const provs: Record = {};
+ for (const [mid, info] of Object.entries(models)) {
+ const p = info.provider || 'other';
+ (provs[p] ??= []).push({ value: mid, name: info.name });
+ }
+ const order = ['openai', 'google', 'meta', 'xai', 'cohere'];
+ const seen = new Set();
+ for (const p of [...order, ...Object.keys(provs)]) {
+ if (seen.has(p) || !provs[p]) continue;
+ seen.add(p);
+ const items = provs[p].filter((i) => !q || i.name.toLowerCase().includes(q));
+ if (items.length) {
+ groups.push({ label: p.charAt(0).toUpperCase() + p.slice(1), items });
+ }
+ }
+ return groups;
+ }, [models, genaiCfg, modelSearch, t]);
+
+ const sessionGroups = useMemo(() => groupSessions(sessions), [sessions]);
+
+ /* ββ Effects ββ */
+
+ // Load sessions on mount
+ useEffect(() => {
+ chatApi.listSessions('chat', 50).then(setSessions).catch(() => {});
+ }, []);
+
+ // Close model dropdown on outside click
+ useEffect(() => {
+ function handleClick(e: MouseEvent) {
+ if (modelDropRef.current && !modelDropRef.current.contains(e.target as Node)) {
+ setModelDropOpen(false);
+ }
+ }
+ document.addEventListener('mousedown', handleClick);
+ return () => document.removeEventListener('mousedown', handleClick);
+ }, []);
+
+ // Keyboard shortcuts
+ useEffect(() => {
+ function handleKey(e: KeyboardEvent) {
+ if (e.key === 'Escape') {
+ setHistoryOpen(false);
+ setSettingsOpen(false);
+ setModelDropOpen(false);
+ }
+ }
+ document.addEventListener('keydown', handleKey);
+ return () => document.removeEventListener('keydown', handleKey);
+ }, []);
+
+ // Auto-scroll
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ }, [messages]);
+
+ // Cleanup poll on unmount
+ useEffect(() => {
+ return () => {
+ if (pollRef.current) clearTimeout(pollRef.current);
+ };
+ }, []);
+
+ /* ββ Actions ββ */
+
+ const refreshSessions = useCallback(() => {
+ chatApi.listSessions('chat', 50).then(setSessions).catch(() => {});
+ }, []);
+
+ const loadSession = useCallback(async (sid: string) => {
+ try {
+ const data = await chatApi.loadSession(sid);
+ setSessionId(sid);
+ setMessages(
+ data.messages.map((m) => ({
+ role: m.role,
+ content: m.content,
+ timestamp: m.created_at?.slice(11, 16) || '',
+ })),
+ );
+ } catch {
+ /* ignore */
+ }
+ }, []);
+
+ const deleteSession = useCallback(
+ async (sid: string) => {
+ try {
+ await chatApi.deleteSession(sid);
+ } catch {
+ /* ignore */
+ }
+ if (sessionId === sid) {
+ setSessionId(null);
+ setMessages([]);
+ }
+ setSessions((prev) => prev.filter((s) => s.id !== sid));
+ },
+ [sessionId],
+ );
+
+ const newChat = useCallback(() => {
+ setSessionId(null);
+ setMessages([]);
+ setFiles([]);
+ if (inputRef.current) inputRef.current.value = '';
+ }, []);
+
+ const pickModel = useCallback(
+ (value: string) => {
+ setModel(value);
+ setModelDropOpen(false);
+ // Auto-select first OCI config for direct models
+ if (value && !value.startsWith('cfg:') && !ociConfig && ociCfg.length) {
+ setOciConfig(ociCfg[0].id);
+ }
+ },
+ [ociConfig, ociCfg],
+ );
+
+ const handleFiles = useCallback(
+ (fileList: FileList | null) => {
+ if (!fileList) return;
+ const newFiles: AttachedFile[] = [];
+ for (const f of Array.from(fileList)) {
+ if (files.length + newFiles.length >= 5) break;
+ const entry: AttachedFile = { file: f, name: f.name, type: f.type.startsWith('image/') ? 'image' : 'file' };
+ if (entry.type === 'image') {
+ entry.preview = URL.createObjectURL(f);
+ }
+ newFiles.push(entry);
+ }
+ setFiles((prev) => [...prev, ...newFiles].slice(0, 5));
+ if (fileInputRef.current) fileInputRef.current.value = '';
+ },
+ [files.length],
+ );
+
+ const removeFile = useCallback((idx: number) => {
+ setFiles((prev) => {
+ const f = prev[idx];
+ if (f.preview) URL.revokeObjectURL(f.preview);
+ return prev.filter((_, i) => i !== idx);
+ });
+ }, []);
+
+ /* ββ Send ββ */
+
+ const sendMessage = useCallback(async () => {
+ const text = inputRef.current?.value.trim() || '';
+ const hasFiles = files.length > 0;
+ if (!text && !hasFiles) return;
+ if (!model) return;
+
+ // Validate OCI config for direct models
+ if (isDirect && !ociConfig) {
+ setMessages((prev) => [
+ ...prev,
+ { role: 'assistant', content: t('chat.selectOciCred'), timestamp: nowHHmm(), failed: true },
+ ]);
+ return;
+ }
+
+ setSending(true);
+ if (inputRef.current) inputRef.current.value = '';
+
+ // User message display
+ let userDisplay = text;
+ if (hasFiles) {
+ const fileList = files.map((f) => f.name).join(', ');
+ userDisplay += (userDisplay ? '\n' : '') + '\u{1F4CE} ' + fileList;
+ }
+
+ setMessages((prev) => [
+ ...prev,
+ { role: 'user', content: userDisplay, timestamp: nowHHmm() },
+ { role: 'assistant', content: '', timestamp: '', thinking: true },
+ ]);
+
+ try {
+ let data: { message_id: string; session_id: string; status: string };
+
+ if (hasFiles) {
+ const fd = new FormData();
+ fd.append('message', text || t('chat.analyzeFiles'));
+ if (sessionId) fd.append('session_id', sessionId);
+ fd.append('use_tools', String(useTools));
+
+ if (model.startsWith('cfg:')) {
+ fd.append('genai_config_id', model.slice(4));
+ } else {
+ fd.append('model_id', model);
+ fd.append('oci_config_id', ociConfig);
+ const oc = ociCfg.find((c) => c.id === ociConfig);
+ if (oc) {
+ fd.append('genai_region', oc.region);
+ fd.append('compartment_id', oc.compartment_id || '');
+ }
+ }
+
+ fd.append('temperature', String(chatParams.temperature));
+ fd.append('max_tokens', String(chatParams.max_tokens));
+ fd.append('top_p', String(chatParams.top_p));
+ fd.append('top_k', String(chatParams.top_k));
+ fd.append('frequency_penalty', String(chatParams.frequency_penalty));
+ fd.append('presence_penalty', String(chatParams.presence_penalty));
+ if (isReasoning && chatParams.reasoning_effort) {
+ fd.append('reasoning_effort', chatParams.reasoning_effort);
+ }
+ files.forEach((f) => fd.append('files', f.file));
+
+ data = await chatApi.upload(fd);
+ setFiles([]);
+ } else {
+ const body: ChatSendBody = {
+ message: text,
+ session_id: sessionId || undefined,
+ use_tools: useTools,
+ };
+
+ if (model.startsWith('cfg:')) {
+ body.genai_config_id = model.slice(4);
+ } else {
+ body.model_id = model;
+ body.oci_config_id = ociConfig;
+ const oc = ociCfg.find((c) => c.id === ociConfig);
+ if (oc) {
+ body.genai_region = oc.region;
+ body.compartment_id = oc.compartment_id || '';
+ }
+ }
+
+ body.temperature = chatParams.temperature;
+ body.max_tokens = chatParams.max_tokens;
+ body.top_p = chatParams.top_p;
+ body.top_k = chatParams.top_k;
+ body.frequency_penalty = chatParams.frequency_penalty;
+ body.presence_penalty = chatParams.presence_penalty;
+ if (isReasoning && chatParams.reasoning_effort) {
+ body.reasoning_effort = chatParams.reasoning_effort;
+ }
+
+ data = await chatApi.send(body);
+ }
+
+ setSessionId(data.session_id);
+
+ if (data.status === 'processing' && data.message_id) {
+ await pollResult(data.message_id);
+ } else {
+ setMessages((prev) => prev.filter((m) => !m.thinking));
+ }
+
+ refreshSessions();
+ } catch (err: any) {
+ setMessages((prev) => [
+ ...prev.filter((m) => !m.thinking),
+ { role: 'assistant', content: `Erro: ${err.message || t('chat.sendError')}`, timestamp: nowHHmm(), failed: true },
+ ]);
+ } finally {
+ setSending(false);
+ inputRef.current?.focus();
+ }
+ }, [model, sessionId, ociConfig, files, chatParams, useTools, isDirect, isReasoning, ociCfg, refreshSessions, t]);
+
+ /* ββ Poll ββ */
+
+ const pollResult = useCallback(
+ async (mid: string) => {
+ for (let i = 0; i < 1200; i++) {
+ await new Promise((resolve) => {
+ pollRef.current = setTimeout(resolve, i < 10 ? 1000 : 3000);
+ });
+ try {
+ const r = await chatApi.pollStatus(mid);
+ if (r.status === 'done') {
+ setMessages((prev) => [
+ ...prev.filter((m) => !m.thinking),
+ { role: 'assistant', content: r.content, timestamp: nowHHmm() },
+ ]);
+ return;
+ }
+ if (r.status === 'failed') {
+ setMessages((prev) => [
+ ...prev.filter((m) => !m.thinking),
+ { role: 'assistant', content: r.content || t('chat.requestFailed'), timestamp: nowHHmm(), failed: true },
+ ]);
+ return;
+ }
+ } catch {
+ /* continue polling */
+ }
+ }
+ // Timeout
+ setMessages((prev) => [
+ ...prev.filter((m) => !m.thinking),
+ { role: 'assistant', content: t('chat.timeout'), timestamp: nowHHmm(), failed: true },
+ ]);
+ },
+ [t],
+ );
+
+ /* ββ Retry ββ */
+
+ const retryMessage = useCallback(
+ (idx: number) => {
+ // Find last user message before this failed one
+ let userMsg = '';
+ for (let i = idx - 1; i >= 0; i--) {
+ if (messages[i].role === 'user') {
+ userMsg = messages[i].content;
+ break;
+ }
+ }
+ if (!userMsg) return;
+ // Remove failed message
+ setMessages((prev) => prev.filter((_, i) => i !== idx));
+ // Put text back in input and send
+ if (inputRef.current) inputRef.current.value = userMsg;
+ // Auto-send after state update
+ setTimeout(() => sendMessage(), 50);
+ },
+ [messages, sendMessage],
+ );
+
+ /* ββ Auto-grow textarea ββ */
+
+ const autoGrow = useCallback((el: HTMLTextAreaElement) => {
+ el.style.height = 'auto';
+ el.style.height = Math.min(el.scrollHeight, 200) + 'px';
+ }, []);
+
+ /* ββ Render ββ */
+
+ return (
+
+ {/* ββ Left Panel: History ββ */}
+
+
+
+ {t('chat.history')}
+
+
+ {t('chat.newChat')}
+
+
+
+ {Object.entries(sessionGroups).map(([group, items]) => (
+
+
+ {group}
+
+ {items.map((s) => (
+
loadSession(s.id)}
+ className="group flex items-center justify-between px-2 py-1.5 rounded cursor-pointer"
+ style={{
+ background: sessionId === s.id ? 'var(--bg3)' : 'transparent',
+ color: sessionId === s.id ? 'var(--t1)' : 'var(--t3)',
+ }}
+ onMouseEnter={(e) => (e.currentTarget.style.background = sessionId === s.id ? 'var(--bg3)' : 'var(--bg2)')}
+ onMouseLeave={(e) => (e.currentTarget.style.background = sessionId === s.id ? 'var(--bg3)' : 'transparent')}
+ >
+
+
{s.title || t('chat.untitled')}
+
+ {timeAgo(s.updated_at || s.created_at)}
+
+
+
{
+ e.stopPropagation();
+ deleteSession(s.id);
+ }}
+ className="opacity-0 group-hover:opacity-100 p-0.5 rounded cursor-pointer"
+ style={{ color: 'var(--rd)' }}
+ >
+
+
+
+ ))}
+
+ ))}
+ {sessions.length === 0 && (
+
+ {t('chat.noConversations')}
+
+ )}
+
+
+
+ {/* ββ Center Panel: Chat ββ */}
+
+ {/* Toolbar */}
+
+ {/* History toggle */}
+
{
+ setHistoryOpen((v) => !v);
+ if (!historyOpen) refreshSessions();
+ }}
+ className="p-1.5 rounded cursor-pointer"
+ style={{ background: historyOpen ? 'var(--bg3)' : 'transparent', color: 'var(--t2)' }}
+ title={t('chat.history')}
+ >
+
+
+
+ {/* Model dropdown */}
+
+
setModelDropOpen((v) => !v)}
+ className="flex items-center gap-1.5 px-2.5 py-1.5 rounded text-xs cursor-pointer"
+ style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t2)', maxWidth: 280 }}
+ >
+ {modelLabel}
+
+
+ {modelDropOpen && (
+
+
+
+
+ setModelSearch(e.target.value)}
+ className="flex-1 bg-transparent text-xs outline-none"
+ style={{ color: 'var(--t2)', border: 'none' }}
+ autoFocus
+ />
+
+
+
+ {modelGroups.map((group) => (
+
+
+ {group.label}
+
+ {group.items.map((item) => (
+
pickModel(item.value)}
+ className="px-3 py-1.5 text-xs cursor-pointer"
+ style={{
+ color: model === item.value ? 'var(--ac)' : 'var(--t2)',
+ background: model === item.value ? 'var(--bg2)' : 'transparent',
+ }}
+ onMouseEnter={(e) => {
+ if (model !== item.value) e.currentTarget.style.background = 'var(--bg2)';
+ }}
+ onMouseLeave={(e) => {
+ if (model !== item.value) e.currentTarget.style.background = 'transparent';
+ }}
+ >
+ {item.name}
+
+ ))}
+
+ ))}
+ {modelGroups.length === 0 && (
+
+ {t('chat.noModels')}
+
+ )}
+
+
+ )}
+
+
+ {/* OCI config selector for direct models */}
+ {isDirect && (
+
setOciConfig(e.target.value)}
+ className="px-2 py-1.5 rounded text-xs"
+ style={{
+ background: 'var(--bg2)',
+ border: '1px solid var(--bd)',
+ color: 'var(--t2)',
+ maxWidth: 200,
+ }}
+ >
+ {t('chat.ociCred')}
+ {ociCfg.map((c) => (
+
+ {c.tenancy_name} ({c.region})
+
+ ))}
+
+ )}
+
+ {/* RAG badge */}
+ {hasRag && (
+
+
+ RAG
+
+ )}
+
+
+
+ {/* Settings toggle */}
+
setSettingsOpen((v) => !v)}
+ className="p-1.5 rounded cursor-pointer"
+ style={{ background: settingsOpen ? 'var(--bg3)' : 'transparent', color: 'var(--t2)' }}
+ title={t('chat.settings')}
+ >
+
+
+
+ {/* New chat */}
+
+
+
+
+
+ {/* Messages area */}
+
+ {messages.length === 0 ? (
+
+
+
+ {t('chat.emptyTitle')}
+
+
+ {t('chat.emptySubtitle')}
+
+
+ ) : (
+
+ {messages.map((msg, idx) => {
+ if (msg.thinking) {
+ return (
+
+
+
+ {t('chat.thinking')}
+
+
+ );
+ }
+
+ const isUser = msg.role === 'user';
+
+ return (
+
+
+ {isUser ? (
+
{msg.content}
+ ) : (
+
+ {msg.content}
+
+ )}
+
+
+ {msg.timestamp && (
+
+ {msg.timestamp}
+
+ )}
+ {msg.failed && (
+ retryMessage(idx)}
+ className="flex items-center gap-1 text-xs cursor-pointer px-1.5 py-0.5 rounded"
+ style={{ color: 'var(--rd)', fontSize: '0.65rem' }}
+ >
+ {t('chat.retry')}
+
+ )}
+
+
+ );
+ })}
+
+
+ )}
+
+
+ {/* File preview chips */}
+ {files.length > 0 && (
+
+ {files.map((f, i) =>
+ f.type === 'image' && f.preview ? (
+
+
+
removeFile(i)}
+ className="absolute flex items-center justify-center rounded-full cursor-pointer"
+ style={{
+ top: -4,
+ right: -4,
+ width: 16,
+ height: 16,
+ background: 'var(--rd)',
+ color: '#fff',
+ fontSize: '0.55rem',
+ }}
+ >
+
+
+
+ ) : (
+
+
+
+ {f.name}
+
+
removeFile(i)} className="cursor-pointer" style={{ color: 'var(--rd)' }}>
+
+
+
+ ),
+ )}
+
+ )}
+
+ {/* Input area */}
+
+
handleFiles(e.target.files)}
+ />
+
fileInputRef.current?.click()}
+ className="p-2 rounded cursor-pointer flex-shrink-0"
+ style={{ color: 'var(--t3)', background: 'var(--bg2)', border: '1px solid var(--bd)' }}
+ title={t('chat.attachFile')}
+ >
+
+
+
+
+
+ {/* ββ Right Panel: Settings ββ */}
+
+
+
+
+ {t('chat.settings')}
+
+
+
+
+ {/* Reasoning models: only max_tokens + reasoning_effort */}
+ {isReasoning ? (
+ <>
+
+ setChatParams((p) => ({ ...p, max_tokens: +e.target.value }))}
+ className="w-full px-2 py-1.5 rounded text-xs"
+ style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t2)' }}
+ />
+
+
+ setChatParams((p) => ({ ...p, reasoning_effort: e.target.value }))}
+ className="w-full px-2 py-1.5 rounded text-xs"
+ style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t2)' }}
+ >
+ Low
+ Medium
+ High
+
+
+ >
+ ) : (
+ <>
+
+
+ setChatParams((p) => ({ ...p, temperature: +e.target.value }))}
+ className="flex-1"
+ style={{ accentColor: 'var(--ac)' }}
+ />
+
+ {chatParams.temperature.toFixed(1)}
+
+
+
+
+ setChatParams((p) => ({ ...p, max_tokens: +e.target.value }))}
+ className="w-full px-2 py-1.5 rounded text-xs"
+ style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t2)' }}
+ />
+
+
+
+ setChatParams((p) => ({ ...p, top_p: +e.target.value }))}
+ className="flex-1"
+ style={{ accentColor: 'var(--ac)' }}
+ />
+
+ {chatParams.top_p.toFixed(2)}
+
+
+
+ {hasTopK && (
+
+ setChatParams((p) => ({ ...p, top_k: +e.target.value }))}
+ className="w-full px-2 py-1.5 rounded text-xs"
+ style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t2)' }}
+ />
+
+ )}
+ {hasPenalties && (
+ <>
+
+
+ setChatParams((p) => ({ ...p, frequency_penalty: +e.target.value }))}
+ className="flex-1"
+ style={{ accentColor: 'var(--ac)' }}
+ />
+
+ {chatParams.frequency_penalty.toFixed(1)}
+
+
+
+
+
+ setChatParams((p) => ({ ...p, presence_penalty: +e.target.value }))}
+ className="flex-1"
+ style={{ accentColor: 'var(--ac)' }}
+ />
+
+ {chatParams.presence_penalty.toFixed(1)}
+
+
+
+ >
+ )}
+ >
+ )}
+
+ {/* MCP Tools */}
+
+
+ setUseTools(e.target.checked)}
+ style={{ accentColor: 'var(--ac)' }}
+ />
+
+ MCP Tools
+
+ {toolCount > 0 && (
+
+ {toolCount} {t('chat.toolsAvailable')}
+
+ )}
+
+
+ {/* OCI config in settings for direct models */}
+ {isDirect && (
+
+
+ setOciConfig(e.target.value)}
+ className="w-full px-2 py-1.5 rounded text-xs"
+ style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t2)' }}
+ >
+ {t('chat.select')}
+ {ociCfg.map((c) => (
+
+ {c.tenancy_name} ({c.region})
+
+ ))}
+
+
+
+ )}
+
+
+
+
+ );
+}
+
+/* ββ Tiny sub-component ββ */
+
+function SettingField({ label, children }: { label: string; children: React.ReactNode }) {
+ return (
+
+
+ {label}
+
+ {children}
+
+ );
+}
diff --git a/frontend-react/src/pages/DownloadsPage.tsx b/frontend-react/src/pages/DownloadsPage.tsx
new file mode 100644
index 0000000..83d2679
--- /dev/null
+++ b/frontend-react/src/pages/DownloadsPage.tsx
@@ -0,0 +1,453 @@
+import { useState, useEffect, useCallback } from 'react';
+import {
+ FolderDown, ChevronRight, RefreshCw, Download, FileText, Lock, Globe,
+ Server, Activity, Database, Package, BarChart3, AlertCircle, Loader2, Filter,
+} from 'lucide-react';
+import client from '@/api/client';
+import { useI18n } from '@/i18n';
+
+/* ββ Types ββ */
+
+interface Report {
+ id: string;
+ tenancy_name: string;
+ status: string;
+ level: number | null;
+ created_at: string;
+ completed_at: string | null;
+}
+
+interface ReportFile {
+ id: string;
+ file_name: string;
+ file_type: string;
+ file_category: string;
+ file_size: number;
+}
+
+/* ββ Helpers ββ */
+
+function extractSection(name: string): string {
+ const m = name.match(/^(?:cis|obp)_([A-Za-z_]+?)_\d/);
+ if (m) return m[1].replace(/_/g, ' ');
+ if (name.includes('summary')) return 'Summary';
+ if (name.includes('error')) return 'Error';
+ return 'Other';
+}
+
+function formatSize(bytes: number): string {
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
+}
+
+function fileExt(name: string): string {
+ return (name.split('.').pop() || '').toLowerCase();
+}
+
+const extColors: Record = {
+ html: '#e44d26',
+ csv: '#217346',
+ json: '#f5a623',
+ xlsx: '#217346',
+ txt: '#6b7280',
+ pdf: '#dc2626',
+};
+
+const sectionIcons: Record = {
+ 'Identity and Access Management': Lock,
+ 'Networking': Globe,
+ 'Compute': Server,
+ 'Logging and Monitoring': Activity,
+ 'Storage Object Storage': Database,
+ 'Storage Block Volumes': Database,
+ 'Storage File Storage Service': Database,
+ 'Asset Management': Package,
+ 'Summary': BarChart3,
+ 'Error': AlertCircle,
+ 'Other': FileText,
+};
+
+function sectionSort(a: string, b: string): number {
+ if (a === 'Summary') return -1;
+ if (b === 'Summary') return 1;
+ if (a === 'Other') return 1;
+ if (b === 'Other') return -1;
+ return a.localeCompare(b);
+}
+
+function groupBySection(files: ReportFile[]): Record {
+ const map: Record = {};
+ for (const f of files) {
+ const sec = extractSection(f.file_name);
+ (map[sec] ||= []).push(f);
+ }
+ return map;
+}
+
+/* ββ Component ββ */
+
+export default function DownloadsPage() {
+ const { t } = useI18n();
+ const [reports, setReports] = useState([]);
+ const [expandedRid, setExpandedRid] = useState(null);
+ const [filesCache, setFilesCache] = useState>({});
+ const [loadingFiles, setLoadingFiles] = useState(null);
+ const [tenancyFilter, setTenancyFilter] = useState('');
+ const [sectionFilter, setSectionFilter] = useState('');
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
+
+ const token = localStorage.getItem('t') || '';
+
+ const loadReports = useCallback(async () => {
+ setLoading(true);
+ setError('');
+ try {
+ const data = await client.get('/reports') as unknown as Report[];
+ setReports(data.filter((r) => r.status === 'completed'));
+ } catch (err) {
+ setError(err instanceof Error ? err.message : t('dl.loadError'));
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => { loadReports(); }, [loadReports]);
+
+ const handleRefresh = useCallback(() => {
+ setFilesCache({});
+ setExpandedRid(null);
+ setSectionFilter('');
+ loadReports();
+ }, [loadReports]);
+
+ const toggleExpand = useCallback(async (rid: string) => {
+ if (expandedRid === rid) {
+ setExpandedRid(null);
+ return;
+ }
+ setExpandedRid(rid);
+ if (!filesCache[rid]) {
+ setLoadingFiles(rid);
+ try {
+ const files = await client.get(`/reports/${rid}/files`) as unknown as ReportFile[];
+ setFilesCache((prev) => ({ ...prev, [rid]: files }));
+ } catch {
+ setFilesCache((prev) => ({ ...prev, [rid]: [] }));
+ } finally {
+ setLoadingFiles(null);
+ }
+ }
+ }, [expandedRid, filesCache]);
+
+ const handleDownload = useCallback((rid: string, fid: string) => {
+ window.open(`/api/reports/${rid}/files/${fid}/download?token=${token}`, '_blank');
+ }, [token]);
+
+ /* Derived data */
+ const tenancies = [...new Set(reports.map((r) => r.tenancy_name))].sort();
+ const filtered = tenancyFilter ? reports.filter((r) => r.tenancy_name === tenancyFilter) : reports;
+
+ /* Sections available in expanded report (for filter) */
+ const expandedFiles = expandedRid ? filesCache[expandedRid] : null;
+ const expandedSections = expandedFiles
+ ? [...new Set(expandedFiles.map((f) => extractSection(f.file_name)))].sort(sectionSort)
+ : [];
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
{t('dl.title')}
+
{t('dl.subtitle')}
+
+
+
{filtered.length} {t('dl.reports')}
+
+
+ {/* Toolbar */}
+
+
+ {/* Tenancy filter */}
+
+
+ { setTenancyFilter(e.target.value); setExpandedRid(null); setSectionFilter(''); }}
+ className="text-xs py-1.5 px-2.5 rounded-lg outline-none cursor-pointer"
+ style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t1)', minWidth: 180 }}
+ >
+ {t('dl.allTenancies')} ({reports.length})
+ {tenancies.map((tn) => (
+
+ {tn} ({reports.filter((r) => r.tenancy_name === tn).length})
+
+ ))}
+
+
+
+ {/* Section filter (only when expanded) */}
+ {expandedRid && expandedSections.length > 1 && (
+
setSectionFilter(e.target.value)}
+ className="text-xs py-1.5 px-2.5 rounded-lg outline-none cursor-pointer"
+ style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t1)', minWidth: 160 }}
+ >
+ {t('dl.allSections')}
+ {expandedSections.map((s) => (
+ {s}
+ ))}
+
+ )}
+
+
+ {filtered.length} {t('dl.reports')}
+
+
+
+
+
+ {t('dl.refresh')}
+
+
+
+ {/* Error */}
+ {error && (
+
+ )}
+
+ {/* Loading */}
+ {loading && !reports.length && (
+
+
+ {t('dl.loading')}
+
+ )}
+
+ {/* Empty state */}
+ {!loading && !filtered.length && (
+
+
+
+
{t('dl.noReports')}
+
{t('dl.noReportsHint')}
+
+
+ )}
+
+ {/* Report list */}
+
+ {filtered.map((report) => {
+ const isExpanded = expandedRid === report.id;
+ const files = filesCache[report.id];
+ const isLoading = loadingFiles === report.id;
+ const fileCount = files ? files.length : null;
+
+ return (
+
+ {/* Report header */}
+
toggleExpand(report.id)}
+ className="w-full flex items-center justify-between px-4 py-3 text-left transition-colors hover:bg-[var(--bg2)]"
+ >
+
+
+
+
+ {report.tenancy_name}
+
+
+
+ Level {report.level || 2}
+
+ |
+
+ {report.created_at}
+
+ {fileCount != null && (
+ <>
+ |
+
+ {fileCount} {t('dl.files')}
+
+ >
+ )}
+
+
+
+
+ {isExpanded ? t('dl.hide') : t('dl.viewFiles')}
+
+
+
+ {/* Expanded file list */}
+ {isExpanded && (
+
+ {isLoading ? (
+
+
+ {t('dl.loadingFiles')}
+
+ ) : !files || !files.length ? (
+
+ {t('dl.noFiles')}
+
+ ) : (
+
+ )}
+
+ )}
+
+ );
+ })}
+
+
+ );
+}
+
+/* ββ FileListGrouped sub-component ββ */
+
+function FileListGrouped({
+ files,
+ reportId,
+ sectionFilter,
+ onDownload,
+}: {
+ files: ReportFile[];
+ reportId: string;
+ sectionFilter: string;
+ onDownload: (rid: string, fid: string) => void;
+}) {
+ const { t } = useI18n();
+ const grouped = groupBySection(files);
+ let sections = Object.keys(grouped).sort(sectionSort);
+ if (sectionFilter) sections = sections.filter((s) => s === sectionFilter);
+
+ return (
+
+ {/* Header */}
+
+
+ {files.length} {t('dl.filesInSections').replace('{0}', String(Object.keys(grouped).length))}
+
+
+
+ {sections.map((sec) => {
+ const secFiles = grouped[sec];
+ const Icon = sectionIcons[sec] || FileText;
+ return (
+
+ {/* Section header */}
+
+
+ {sec}
+ ({secFiles.length})
+
+
+ {/* File grid */}
+
+ {secFiles.map((f) => {
+ const ext = fileExt(f.file_name);
+ const color = extColors[ext] || 'var(--ac)';
+ return (
+
onDownload(reportId, f.id)}
+ className="flex items-center gap-2.5 px-3 py-2.5 rounded-lg text-left transition-all group"
+ style={{
+ background: 'var(--bg)',
+ border: '1px solid var(--bd)',
+ }}
+ onMouseEnter={(e) => {
+ e.currentTarget.style.borderColor = color;
+ e.currentTarget.style.boxShadow = `0 2px 8px ${color}18`;
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.borderColor = 'var(--bd)';
+ e.currentTarget.style.boxShadow = 'none';
+ }}
+ >
+ {/* File type badge */}
+
+
+ {ext}
+
+
+ {/* File info */}
+
+
+ {f.file_name}
+
+
+ {formatSize(f.file_size)}
+
+
+ {/* Download icon */}
+
+
+ );
+ })}
+
+
+ );
+ })}
+
+ );
+}
diff --git a/frontend-react/src/pages/ExplorerPage.tsx b/frontend-react/src/pages/ExplorerPage.tsx
new file mode 100644
index 0000000..3438d52
--- /dev/null
+++ b/frontend-react/src/pages/ExplorerPage.tsx
@@ -0,0 +1,1593 @@
+import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
+import {
+ Search, ChevronRight, ChevronDown, FolderTree, RefreshCw, Loader2,
+ Server, HardDrive, Layers, Network, GitBranch, Shield, ShieldCheck,
+ Route, ArrowUpRight, Globe, Plug, MapPin, Scale, Database, FolderOpen,
+ Link, Box, Boxes, Zap, Webhook, Bell, FileText, MessageSquare, Activity,
+ Lock, Flame, FileCheck, User, Users, UserCog, Play, Square, Package,
+ ChevronUp, Check, X, Home, Info, Plus, Trash2, Crosshair,
+} from 'lucide-react';
+import { useAppStore } from '@/stores/app';
+import { useI18n } from '@/i18n';
+import { explorerApi, type CompartmentNode, type Region, type ResourceItem } from '@/api/endpoints/explorer';
+
+/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ Constants
+ ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+const ICON_MAP: Record = {
+ Server, HardDrive, Layers, Network, GitBranch, Shield, ShieldCheck,
+ Route, ArrowUpRight, Globe, Plug, MapPin, Scale, Database, FolderOpen,
+ Link, Box, Boxes, Zap, Webhook, Bell, FileText, MessageSquare, Activity,
+ Lock, Flame, FileCheck, User, Users, UserCog,
+};
+
+interface ResType { key: string; label: string; icon: string }
+
+const EXP_GROUPS: Record = {
+ 'Compute': [
+ { key: 'instances', label: 'Instances', icon: 'Server' },
+ { key: 'boot_volumes', label: 'Boot Volumes', icon: 'HardDrive' },
+ { key: 'instance_pools', label: 'Instance Pools', icon: 'Layers' },
+ ],
+ 'Rede': [
+ { key: 'vcns', label: 'VCNs', icon: 'Network' },
+ { key: 'subnets', label: 'Subnets', icon: 'GitBranch' },
+ { key: 'security_lists', label: 'Security Lists', icon: 'Shield' },
+ { key: 'nsgs', label: 'NSGs', icon: 'ShieldCheck' },
+ { key: 'route_tables', label: 'Route Tables', icon: 'Route' },
+ { key: 'nat_gateways', label: 'NAT Gateways', icon: 'ArrowUpRight' },
+ { key: 'internet_gateways', label: 'Internet Gateways', icon: 'Globe' },
+ { key: 'service_gateways', label: 'Service Gateways', icon: 'Plug' },
+ { key: 'public_ips', label: 'Public IPs', icon: 'MapPin' },
+ { key: 'load_balancers', label: 'Load Balancers', icon: 'Scale' },
+ ],
+ 'Storage': [
+ { key: 'buckets', label: 'Buckets', icon: 'Database' },
+ { key: 'block_volumes', label: 'Block Volumes', icon: 'HardDrive' },
+ { key: 'file_systems', label: 'File Systems', icon: 'FolderOpen' },
+ { key: 'mount_targets', label: 'Mount Targets', icon: 'Link' },
+ ],
+ 'Database': [
+ { key: 'databases', label: 'Autonomous DB', icon: 'Database' },
+ { key: 'db_systems', label: 'DB Systems', icon: 'Server' },
+ { key: 'mysql_db_systems', label: 'MySQL', icon: 'Database' },
+ ],
+ 'Containers': [
+ { key: 'container_instances', label: 'Container Instances', icon: 'Box' },
+ { key: 'oke_clusters', label: 'OKE Clusters', icon: 'Boxes' },
+ ],
+ 'Serverless': [
+ { key: 'functions', label: 'Functions', icon: 'Zap' },
+ { key: 'api_gateways', label: 'API Gateways', icon: 'Webhook' },
+ ],
+ 'Observabilidade': [
+ { key: 'alarms', label: 'Alarms', icon: 'Bell' },
+ { key: 'log_groups', label: 'Log Groups', icon: 'FileText' },
+ { key: 'notification_topics', label: 'Notification Topics', icon: 'MessageSquare' },
+ { key: 'events_rules', label: 'Events Rules', icon: 'Activity' },
+ ],
+ 'Seguranca': [
+ { key: 'vaults', label: 'Vaults', icon: 'Lock' },
+ { key: 'dns_zones', label: 'DNS Zones', icon: 'Globe' },
+ { key: 'network_firewalls', label: 'Network Firewalls', icon: 'Flame' },
+ { key: 'network_firewall_policies', label: 'Firewall Policies', icon: 'FileCheck' },
+ ],
+ 'IAM': [
+ { key: 'iam_users', label: 'Users', icon: 'User' },
+ { key: 'iam_groups', label: 'Groups', icon: 'Users' },
+ { key: 'iam_policies', label: 'Policies', icon: 'FileText' },
+ { key: 'iam_dynamic_groups', label: 'Dynamic Groups', icon: 'UserCog' },
+ ],
+};
+
+const CATEGORIES = Object.keys(EXP_GROUPS);
+
+const CATEGORY_ICONS: Record = {
+ Compute: Server, Rede: Network, Storage: Database, Database: Database,
+ Containers: Box, Serverless: Zap, Observabilidade: Bell, Seguranca: Lock, IAM: Users,
+};
+
+const CATEGORY_I18N: Record = {
+ Rede: 'exp.network',
+ Observabilidade: 'exp.observability',
+ Seguranca: 'exp.security',
+};
+
+const STARTABLE: Record = {
+ instances: { running: ['RUNNING'], stopped: ['STOPPED'], actionType: 'instance' },
+ databases: { running: ['AVAILABLE'], stopped: ['STOPPED'], actionType: 'adb' },
+ db_systems: { running: ['AVAILABLE'], stopped: ['STOPPED'], actionType: 'dbSystem' },
+ mysql_db_systems: { running: ['ACTIVE'], stopped: ['INACTIVE'], actionType: 'mysql' },
+ container_instances: { running: ['ACTIVE'], stopped: ['INACTIVE'], actionType: 'container' },
+};
+
+/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ Helpers
+ ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+interface TreeNode extends CompartmentNode {
+ children: TreeNode[];
+ depth: number;
+}
+
+function buildTree(flat: CompartmentNode[]): TreeNode[] {
+ // Same logic as legacy rExpTree: filter by parent_id, recursive
+ const byParent = new Map();
+ for (const n of flat) {
+ const key = n.parent_id ?? '__root__';
+ const arr = byParent.get(key) || [];
+ arr.push(n);
+ byParent.set(key, arr);
+ }
+ const build = (parentId: string | null, depth: number): TreeNode[] => {
+ const key = parentId ?? '__root__';
+ const children = byParent.get(key) || [];
+ return children.map((n) => ({
+ ...n,
+ depth,
+ children: build(n.id, depth + 1),
+ }));
+ };
+ return build(null, 0);
+}
+
+function formatBytes(bytes: number | null | undefined): string {
+ if (bytes == null || bytes === 0) return '0 B';
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
+ return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
+}
+
+function lifecycleColor(state: string): { bg: string; text: string } {
+ const s = (state || '').toUpperCase();
+ if (['RUNNING', 'ACTIVE', 'AVAILABLE'].includes(s))
+ return { bg: 'var(--gnl)', text: 'var(--gn)' };
+ if (['STOPPED', 'INACTIVE'].includes(s))
+ return { bg: 'var(--yll)', text: 'var(--yl)' };
+ if (['TERMINATED', 'DELETED', 'DELETING', 'TERMINATING'].includes(s))
+ return { bg: 'var(--rdl)', text: 'var(--rd)' };
+ return { bg: 'var(--bg3)', text: 'var(--t3)' };
+}
+
+function truncId(id: string | undefined): string {
+ if (!id) return '';
+ if (id.length <= 30) return id;
+ return id.slice(0, 14) + '...' + id.slice(-14);
+}
+
+/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ Sub-components
+ ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+/* ββ Lifecycle Badge ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+function StateBadge({ state }: { state: string }) {
+ const c = lifecycleColor(state);
+ return (
+
+ {state}
+
+ );
+}
+
+/* ββ KV Row (generic card helper) βββββββββββββββββββββββββββββββββββββββββββ */
+function KV({ label, value, mono }: { label: string; value: React.ReactNode; mono?: boolean }) {
+ return (
+
+ {label}
+
+ {value ?? 'β'}
+
+
+ );
+}
+
+/* ββ Compartment Tree Node ββββββββββββββββββββββββββββββββββββββββββββββββββ */
+function TreeItem({
+ node, selected, expanded, onSelect, onToggle,
+}: {
+ node: TreeNode;
+ selected: string;
+ expanded: Record;
+ onSelect: (id: string) => void;
+ onToggle: (id: string) => void;
+}) {
+ const hasKids = node.children.length > 0;
+ const isOpen = expanded[node.id];
+ const isSel = selected === node.id;
+
+ return (
+
+
{ onSelect(node.id); if (hasKids && !isOpen) onToggle(node.id); }}
+ onMouseEnter={(e) => { if (!isSel) e.currentTarget.style.background = 'var(--bg2)'; }}
+ onMouseLeave={(e) => { if (!isSel) e.currentTarget.style.background = ''; }}
+ >
+ { e.stopPropagation(); if (hasKids) onToggle(node.id); }}
+ >
+ {hasKids ? (isOpen ? 'βΌ' : 'βΆ') : ' '}
+
+
+ {node.name}
+
+
+ {hasKids && isOpen && (
+
+ {node.children.map((c) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+/* ββ Region Multi-Select ββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+function RegionSelector({
+ regions, selected, onChange,
+}: {
+ regions: Region[];
+ selected: string[];
+ onChange: (v: string[]) => void;
+}) {
+ const { t } = useI18n();
+ const [open, setOpen] = useState(false);
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const handler = (e: MouseEvent) => {
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
+ };
+ document.addEventListener('mousedown', handler);
+ return () => document.removeEventListener('mousedown', handler);
+ }, []);
+
+ const toggle = (name: string) => {
+ onChange(selected.includes(name) ? selected.filter((r) => r !== name) : [...selected, name]);
+ };
+
+ return (
+
+
setOpen(!open)}
+ >
+ {selected.length ? t('exp.nRegions').replace('{0}', String(selected.length)) : t('exp.selectRegions')}
+ {open ? : }
+
+ {open && (
+
+
+ onChange(regions.map((r) => r.name))}
+ >{t('exp.all')}
+ onChange([])}
+ >{t('exp.none')}
+
+ {regions.map((r) => (
+
+ toggle(r.name)}
+ className="accent-current"
+ style={{ accentColor: 'var(--ac)' }}
+ />
+ {r.name}
+ {r.is_home && }
+
+ ))}
+
+ )}
+
+ );
+}
+
+/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ Special Card Renderers
+ ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+function VcnCard({ r }: { r: ResourceItem }) {
+ const { t } = useI18n();
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+}
+
+function SubnetCard({ r }: { r: ResourceItem }) {
+ const { t } = useI18n();
+ const slIds = (r.security_list_ids || []) as string[];
+ return (
+ <>
+
+
+
+
+
+
+
+ {slIds.length > 0 && (
+
+
Security Lists
+
+ {slIds.map((id, i) => (
+ {truncId(id)}
+ ))}
+
+
+ )}
+
+ >
+ );
+}
+
+function InstanceCard({ r }: { r: ResourceItem }) {
+ const { t } = useI18n();
+ return (
+ <>
+
+
+
+ >
+ );
+}
+
+function AdbCard({ r, configId, regions, onReload }: { r: ResourceItem; configId: string; regions: string[]; onReload: () => void }) {
+ const { t } = useI18n();
+ const [updating, setUpdating] = useState(false);
+ const [msg, setMsg] = useState<{ type: 's' | 'e'; text: string } | null>(null);
+ const ips = (r.whitelisted_ips || []) as string[];
+ const region = regions.length === 1 ? regions[0] : undefined;
+
+ const handleUpdateNetwork = async () => {
+ setMsg(null);
+ try {
+ const resp = await fetch('https://api.ipify.org?format=json');
+ const data = await resp.json();
+ const myIp = data.ip + '/32';
+ const ip = prompt('IP para liberar acesso (CIDR):', myIp);
+ if (!ip) return;
+ setUpdating(true);
+ await explorerApi.adbUpdateNetwork(r.id, ip.trim(), configId, region);
+ setMsg({ type: 's', text: t('exp.accessUpdated') + ip.trim() });
+ onReload();
+ } catch (e) {
+ setMsg({ type: 'e', text: (e as Error).message });
+ } finally {
+ setUpdating(false);
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+ {ips.length > 0 && (
+
+
ACL
+
+ {ips.map((ip, i) => (
+ {ip}
+ ))}
+
+
+ )}
+ {ips.length === 0 && }
+
+
+ {updating ? : }
+ {t('exp.updateNetworkAccess')}
+
+
+ {msg && {msg.text}
}
+ >
+ );
+}
+
+function DbSystemCard({ r }: { r: ResourceItem }) {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+function MysqlCard({ r }: { r: ResourceItem }) {
+ const { t } = useI18n();
+ return (
+ <>
+
+
+
+ >
+ );
+}
+
+function ContainerCard({ r }: { r: ResourceItem }) {
+ const { t } = useI18n();
+ return (
+ <>
+
+
+
+ >
+ );
+}
+
+function BucketCard({ r }: { r: ResourceItem }) {
+ const { t } = useI18n();
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+}
+
+function LoadBalancerCard({ r }: { r: ResourceItem }) {
+ const { t } = useI18n();
+ return (
+ <>
+
+
+
+ {(r.listeners || []).length > 0 && (
+
+
Listeners
+
+
+
+
+ {t('exp.description')}
+ {t('exp.ports')}
+ {t('exp.protocol')}
+ Backend Set
+
+
+
+ {(r.listeners as Array<{name:string;port:number;protocol:string;backend_set:string}>).map((l, i) => (
+
+ {l.name}
+ {l.port}
+ {l.protocol}
+ {l.backend_set}
+
+ ))}
+
+
+
+
+ )}
+ {(r.backend_sets || []).length > 0 && (
+
+
Backend Sets
+ {(r.backend_sets as Array<{name:string;policy:string;backends:Array<{ip:string;port:number;weight:number}>}>).map((bs, i) => (
+
+
{bs.name}
+
({bs.policy})
+ {(bs.backends || []).map((b, j) => (
+
+ {b.ip}:{b.port} (w={b.weight})
+
+ ))}
+
+ ))}
+
+ )}
+ >
+ );
+}
+
+/* ββ Rule Form Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+function RuleSelect({ label, value, onChange, options }: { label: string; value: string; onChange: (v: string) => void; options: string[][] }) {
+ return (
+
+ {label}
+ onChange(e.target.value)} className="w-full px-2 py-1 rounded text-xs border outline-none" style={{ background: 'var(--bg)', borderColor: 'var(--bd)', color: 'var(--t1)' }}>
+ {options.map(([v, l]) => {l} )}
+
+
+ );
+}
+
+function RuleInput({ label, value, onChange, placeholder }: { label: string; value: string; onChange: (v: string) => void; placeholder?: string }) {
+ return (
+
+ {label}
+ onChange(e.target.value)} placeholder={placeholder} className="w-full px-2 py-1 rounded text-xs border outline-none" style={{ background: 'var(--bg)', borderColor: 'var(--bd)', color: 'var(--t1)' }} />
+
+ );
+}
+
+function SecurityListCard({ r, configId, regions, onReload }: { r: ResourceItem; configId: string; regions: string[]; onReload: () => void }) {
+ const { t } = useI18n();
+ const [expanded, setExpanded] = useState(false);
+ const [showForm, setShowForm] = useState(false);
+ const [formMsg, setFormMsg] = useState<{ type: 's' | 'e'; text: string } | null>(null);
+ const [saving, setSaving] = useState(false);
+ const [dir, setDir] = useState('ingress');
+ const [proto, setProto] = useState('6');
+ const [cidr, setCidr] = useState('');
+ const [portMin, setPortMin] = useState('');
+ const [portMax, setPortMax] = useState('');
+ const [desc, setDesc] = useState('');
+ const [stateless, setStateless] = useState(false);
+
+ const rules = (r.rules || []) as Array<{
+ direction: string; protocol: string; source_dest: string;
+ ports: string; stateless: boolean; description: string; rule_index?: number;
+ }>;
+ const ingress = rules.filter((x) => x.direction === 'ingress');
+ const egress = rules.filter((x) => x.direction === 'egress');
+ const region = regions.length === 1 ? regions[0] : undefined;
+
+ const handleAdd = async () => {
+ if (!cidr.trim()) { setFormMsg({ type: 'e', text: t('exp.enterCidr') }); return; }
+ setSaving(true); setFormMsg(null);
+ try {
+ const body: Record = { direction: dir, protocol: proto, source_dest: cidr.trim(), description: desc, is_stateless: stateless, region };
+ if (proto !== 'all' && proto !== '1' && portMin) { body.port_min = parseInt(portMin); body.port_max = parseInt(portMax || portMin); }
+ await explorerApi.addSlRule(configId, r.id, body as Parameters[2]);
+ setFormMsg({ type: 's', text: t('exp.ruleAdded') }); setShowForm(false); setCidr(''); setPortMin(''); setPortMax(''); setDesc('');
+ onReload();
+ } catch (e) { setFormMsg({ type: 'e', text: (e as Error).message }); }
+ finally { setSaving(false); }
+ };
+
+ const handleRemove = async (direction: string, ruleIndex: number) => {
+ if (!confirm(t('exp.removeRule'))) return;
+ try {
+ await explorerApi.removeSlRule(configId, r.id, { direction, rule_index: ruleIndex, region });
+ onReload();
+ } catch (e) { alert((e as Error).message); }
+ };
+
+ const handleMyIp = async () => {
+ try {
+ const resp = await fetch('https://api.ipify.org?format=json');
+ const data = await resp.json();
+ const myIp = data.ip + '/32';
+ setCidr(myIp); setDesc('Acesso temporΓ‘rio - ' + myIp);
+ setFormMsg({ type: 's', text: t('exp.ipDetected').replace('{0}', myIp) });
+ } catch { setFormMsg({ type: 'e', text: t('exp.ipDetectFailed') }); }
+ };
+
+ return (
+ <>
+
+
+
+
+ {rules.length > 0 && (
+
setExpanded(!expanded)}>
+ {expanded ? t('exp.hideRules') : t('exp.viewRules')}
+
+ )}
+
{ setShowForm(!showForm); setFormMsg(null); }}
+ >
+ {t('exp.add')}
+
+
+ {showForm && (
+
+
+
+
+
+
+
+
+ {t('exp.myIp')}
+
+
+ {proto !== 'all' && proto !== '1' && (
+
+
+
+
+ )}
+
+
+ setStateless(e.target.checked)} className="accent-current" style={{ accentColor: 'var(--ac)' }} />
+ Stateless
+
+ {formMsg &&
{formMsg.text}
}
+
+
+ {saving ? : } {t('exp.save')}
+
+ setShowForm(false)} className="px-3 py-1 rounded text-xs" style={{ background: 'var(--bg3)', color: 'var(--t2)' }}>{t('exp.cancel')}
+
+
+ )}
+ {expanded && (
+
+ {[{ label: 'Ingress', dir: 'ingress', data: ingress }, { label: 'Egress', dir: 'egress', data: egress }].map(({ label, dir: d, data }) =>
+ data.length > 0 && (
+
+
{label}
+
+
+
+ {t('exp.protocol')}
+ {t('exp.originDest')}
+ {t('exp.ports')}
+ Stateless
+ {t('exp.description')}
+
+
+
+
+ {data.map((rule, i) => (
+
+ {rule.protocol}
+ {rule.source_dest || '*'}
+ {rule.ports || 'ALL'}
+ {rule.stateless ? t('exp.yes') : t('exp.no')}
+ {rule.description || 'β'}
+
+ handleRemove(d, i)} className="p-0.5 rounded hover:opacity-80" style={{ color: 'var(--rd)' }} title={t('common.delete')}>
+
+
+
+
+ ))}
+
+
+
+ )
+ )}
+
+ )}
+ >
+ );
+}
+
+function NsgCard({ r, configId, regions, onReload }: { r: ResourceItem; configId: string; regions: string[]; onReload: () => void }) {
+ const { t } = useI18n();
+ const [expanded, setExpanded] = useState(false);
+ const [showForm, setShowForm] = useState(false);
+ const [formMsg, setFormMsg] = useState<{ type: 's' | 'e'; text: string } | null>(null);
+ const [saving, setSaving] = useState(false);
+ const [dir, setDir] = useState('INGRESS');
+ const [proto, setProto] = useState('6');
+ const [cidr, setCidr] = useState('');
+ const [portMin, setPortMin] = useState('');
+ const [portMax, setPortMax] = useState('');
+ const [desc, setDesc] = useState('');
+ const [stateless, setStateless] = useState(false);
+
+ const rules = (r.rules || []) as Array<{
+ id: string; direction: string; protocol: string;
+ source_dest: string; source_dest_type: string;
+ ports: string; is_stateless: boolean; description: string;
+ }>;
+ const region = regions.length === 1 ? regions[0] : undefined;
+
+ const handleAdd = async () => {
+ if (!cidr.trim()) { setFormMsg({ type: 'e', text: t('exp.enterCidr') }); return; }
+ setSaving(true); setFormMsg(null);
+ try {
+ const body: Record = { direction: dir, protocol: proto, source_dest: cidr.trim(), source_dest_type: 'CIDR_BLOCK', description: desc, is_stateless: stateless, region };
+ if (proto !== 'all' && proto !== '1' && portMin) { body.port_min = parseInt(portMin); body.port_max = parseInt(portMax || portMin); }
+ await explorerApi.addNsgRule(configId, r.id, body as Parameters[2]);
+ setFormMsg({ type: 's', text: t('exp.ruleAdded') }); setShowForm(false); setCidr(''); setPortMin(''); setPortMax(''); setDesc('');
+ onReload();
+ } catch (e) { setFormMsg({ type: 'e', text: (e as Error).message }); }
+ finally { setSaving(false); }
+ };
+
+ const handleRemove = async (ruleId: string) => {
+ if (!confirm(t('exp.removeNsgRule'))) return;
+ try {
+ await explorerApi.removeNsgRule(configId, r.id, ruleId, region);
+ onReload();
+ } catch (e) { alert((e as Error).message); }
+ };
+
+ const handleMyIp = async () => {
+ try {
+ const resp = await fetch('https://api.ipify.org?format=json');
+ const data = await resp.json();
+ const myIp = data.ip + '/32';
+ setCidr(myIp); setDesc('Acesso temporΓ‘rio - ' + myIp);
+ setFormMsg({ type: 's', text: t('exp.ipDetected').replace('{0}', myIp) });
+ } catch { setFormMsg({ type: 'e', text: t('exp.ipDetectFailed') }); }
+ };
+
+ return (
+ <>
+
+
+
+
+ {rules.length > 0 && (
+
setExpanded(!expanded)}>
+ {expanded ? t('exp.hideRules') : t('exp.viewRules')}
+
+ )}
+
{ setShowForm(!showForm); setFormMsg(null); }}
+ >
+ {t('exp.add')}
+
+
+ {showForm && (
+
+
+
+
+
+
+
+
+ {t('exp.myIp')}
+
+
+ {proto !== 'all' && proto !== '1' && (
+
+
+
+
+ )}
+
+
+ setStateless(e.target.checked)} className="accent-current" style={{ accentColor: 'var(--ac)' }} />
+ Stateless
+
+ {formMsg &&
{formMsg.text}
}
+
+
+ {saving ? : } {t('exp.save')}
+
+ setShowForm(false)} className="px-3 py-1 rounded text-xs" style={{ background: 'var(--bg3)', color: 'var(--t2)' }}>{t('exp.cancel')}
+
+
+ )}
+ {expanded && (
+
+
+
+
+ {t('exp.direction')}
+ {t('exp.protocol')}
+ {t('exp.originDest')}
+ Tipo
+ {t('exp.ports')}
+ {t('exp.description')}
+
+
+
+
+ {rules.map((rule, i) => (
+
+ {rule.direction}
+ {rule.protocol}
+ {rule.source_dest || '*'}
+ {rule.source_dest_type}
+ {rule.ports || 'ALL'}
+ {rule.description || 'β'}
+
+ handleRemove(rule.id)} className="p-0.5 rounded hover:opacity-80" style={{ color: 'var(--rd)' }} title={t('common.delete')}>
+
+
+
+
+ ))}
+
+
+
+ )}
+ >
+ );
+}
+
+function RouteTableCard({ r }: { r: ResourceItem }) {
+ const { t } = useI18n();
+ const routes = (r.routes || []) as Array<{
+ destination: string; destination_type: string;
+ target_id: string; target_type: string; description: string;
+ }>;
+
+ return (
+ <>
+
+
+ {routes.length > 0 && (
+
+
+
+
+ {t('exp.destination')}
+ {t('exp.destType')}
+ Target Type
+ {t('exp.description')}
+
+
+
+ {routes.map((rt, i) => (
+
+ {rt.destination}
+ {rt.destination_type}
+ {rt.target_type}
+ {rt.description || 'β'}
+
+ ))}
+
+
+
+ )}
+ >
+ );
+}
+
+/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ Main Page
+ ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+export default function ExplorerPage() {
+ const { ociCfg } = useAppStore();
+ const { t } = useI18n();
+
+ /* ββ State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+ const [selectedConfig, setSelectedConfig] = useState('');
+ const [compartmentTree, setCompartmentTree] = useState([]);
+ const [selectedCompartment, setSelectedCompartment] = useState('');
+ const [selectedRegions, setSelectedRegions] = useState([]);
+ const [availableRegions, setAvailableRegions] = useState([]);
+ const [selectedCategory, setSelectedCategory] = useState('Compute');
+ const [selectedResourceType, setSelectedResourceType] = useState('instances');
+ const [resourceData, setResourceData] = useState(null);
+ const [counts, setCounts] = useState>({});
+ const [loading, setLoading] = useState(false);
+ const [countsRefreshing, setCountsRefreshing] = useState(false);
+ const [treeOpen, setTreeOpen] = useState>({});
+ const [treeWidth, setTreeWidth] = useState(280);
+ const [treeLoading, setTreeLoading] = useState(false);
+ const [actionPending, setActionPending] = useState>({});
+ const [searchFilter, setSearchFilter] = useState('');
+
+ const containerRef = useRef(null);
+ const draggingRef = useRef(false);
+
+ /* ββ Pick first config ββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+ useEffect(() => {
+ if (ociCfg.length > 0 && !selectedConfig) setSelectedConfig(ociCfg[0].id);
+ }, [ociCfg, selectedConfig]);
+
+ /* ββ Load tree + regions on config change βββββββββββββββββββββββββββββββββ */
+ useEffect(() => {
+ if (!selectedConfig) return;
+ let cancelled = false;
+ setTreeLoading(true);
+ setCompartmentTree([]);
+ setSelectedCompartment('');
+ setCounts({});
+ setResourceData(null);
+
+ (async () => {
+ try {
+ const [flat, regs] = await Promise.all([
+ explorerApi.getCompartmentTree(selectedConfig),
+ explorerApi.getRegions(selectedConfig),
+ ]);
+ if (cancelled) return;
+ if (Array.isArray(flat)) {
+ const tree = buildTree(flat);
+ setCompartmentTree(tree);
+ // Auto-select first node and expand it
+ if (tree.length > 0) {
+ setSelectedCompartment(tree[0].id);
+ setTreeOpen((prev) => ({ ...prev, [tree[0].id]: true }));
+ }
+ }
+ if (Array.isArray(regs)) {
+ setAvailableRegions(regs);
+ const home = regs.find((r) => r.is_home);
+ setSelectedRegions(home ? [home.name] : regs.length > 0 ? [regs[0].name] : []);
+ }
+ } catch {
+ /* ignore */
+ } finally {
+ if (!cancelled) setTreeLoading(false);
+ }
+ })();
+ return () => { cancelled = true; };
+ }, [selectedConfig]);
+
+ /* ββ Load counts when compartment or regions change βββββββββββββββββββββββ */
+ useEffect(() => {
+ if (!selectedConfig || !selectedCompartment) return;
+ let cancelled = false;
+ const regionsKey = [...selectedRegions].sort().join(',');
+ (async () => {
+ try {
+ const data = await explorerApi.getCounts(selectedConfig, selectedCompartment, regionsKey);
+ if (cancelled) return;
+ if (data && typeof data === 'object' && !('error' in data)) {
+ const simple: Record = {};
+ for (const [k, v] of Object.entries(data)) {
+ simple[k] = typeof v === 'object' && v !== null ? (v as { count: number }).count : (v as number);
+ }
+ setCounts(simple);
+ }
+ } catch { /* ignore */ }
+ })();
+ return () => { cancelled = true; };
+ }, [selectedConfig, selectedCompartment, selectedRegions]);
+
+ /* ββ Load resources when type or compartment or regions change βββββββββββββ */
+ const loadResources = useCallback(async (resType?: string, silent = false) => {
+ const rt = resType || selectedResourceType;
+ if (!selectedConfig || !selectedCompartment) return;
+ if (!silent) {
+ setLoading(true);
+ setResourceData(null);
+ }
+
+ try {
+ // Load from all selected regions in batches of 3
+ const regions = selectedRegions.length > 0 ? [...selectedRegions] : [undefined];
+ const allItems: ResourceItem[] = [];
+
+ for (let i = 0; i < regions.length; i += 3) {
+ const batch = regions.slice(i, i + 3);
+ const results = await Promise.all(
+ batch.map((reg) =>
+ explorerApi.getResources(selectedConfig, rt, selectedCompartment, reg)
+ .catch(() => [])
+ )
+ );
+ for (const res of results) {
+ if (Array.isArray(res)) allItems.push(...res);
+ }
+ if (i + 3 < regions.length) {
+ await new Promise((resolve) => setTimeout(resolve, 800));
+ }
+ }
+ setResourceData(allItems);
+ } catch {
+ if (!silent) setResourceData([]);
+ } finally {
+ if (!silent) setLoading(false);
+ }
+ }, [selectedConfig, selectedCompartment, selectedRegions, selectedResourceType]);
+
+ useEffect(() => {
+ if (selectedCompartment && selectedResourceType) {
+ loadResources();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [selectedCompartment, selectedResourceType, selectedRegions]);
+
+ /* ββ Refresh Counts βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+ const refreshCounts = useCallback(async () => {
+ if (!selectedConfig || !selectedCompartment || countsRefreshing) return;
+ setCountsRefreshing(true);
+ try {
+ await explorerApi.refreshCounts(selectedConfig, selectedCompartment, selectedRegions);
+ // Poll
+ let attempts = 0;
+ const poll = setInterval(async () => {
+ attempts++;
+ const regionsKey = [...selectedRegions].sort().join(',');
+ try {
+ const data = await explorerApi.getCounts(selectedConfig, selectedCompartment, regionsKey);
+ if (data && typeof data === 'object' && !('error' in data)) {
+ const simple: Record = {};
+ for (const [k, v] of Object.entries(data)) {
+ simple[k] = typeof v === 'object' && v !== null ? (v as { count: number }).count : (v as number);
+ }
+ setCounts(simple);
+ }
+ } catch { /* ignore */ }
+ if (attempts >= 12) {
+ clearInterval(poll);
+ setCountsRefreshing(false);
+ }
+ }, 3000);
+ // Also clear after max time
+ setTimeout(() => { clearInterval(poll); setCountsRefreshing(false); }, 40000);
+ } catch {
+ setCountsRefreshing(false);
+ }
+ }, [selectedConfig, selectedCompartment, selectedRegions, countsRefreshing]);
+
+ /* ββ Start/Stop Action ββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+ const handleAction = useCallback(async (item: ResourceItem, actionStr: string) => {
+ const itemId = item.id || item.name;
+ if (!itemId || actionPending[itemId]) return;
+
+ setActionPending((p) => ({ ...p, [itemId]: true }));
+
+ try {
+ const cfg = STARTABLE[selectedResourceType];
+ if (!cfg) return;
+
+ switch (cfg.actionType) {
+ case 'instance':
+ await explorerApi.instanceAction(itemId, actionStr as 'START' | 'STOP', selectedConfig);
+ break;
+ case 'adb':
+ await explorerApi.adbAction(itemId, actionStr.toLowerCase() as 'start' | 'stop', selectedConfig);
+ break;
+ case 'dbSystem':
+ await explorerApi.dbSystemAction(itemId, actionStr as 'START' | 'STOP', selectedConfig);
+ break;
+ case 'mysql':
+ await explorerApi.mysqlAction(itemId, actionStr.toLowerCase() as 'start' | 'stop', selectedConfig);
+ break;
+ case 'container':
+ await explorerApi.containerAction(itemId, actionStr.toLowerCase() as 'start' | 'stop', selectedConfig);
+ break;
+ }
+
+ // Poll for state change (silent = no flickering)
+ let attempts = 0;
+ const poll = setInterval(async () => {
+ attempts++;
+ try {
+ await loadResources(undefined, true);
+ } catch { /* ignore */ }
+ if (attempts >= 12) {
+ clearInterval(poll);
+ setActionPending((p) => ({ ...p, [itemId]: false }));
+ }
+ }, 5000);
+ setTimeout(() => { clearInterval(poll); setActionPending((p) => ({ ...p, [itemId]: false })); }, 65000);
+ } catch {
+ setActionPending((p) => ({ ...p, [itemId]: false }));
+ }
+ }, [selectedConfig, selectedResourceType, actionPending, loadResources]);
+
+ /* ββ Resizable tree panel βββββββββββββββββββββββββββββββββββββββββββββββββ */
+ useEffect(() => {
+ const onMove = (e: MouseEvent) => {
+ if (!draggingRef.current || !containerRef.current) return;
+ const rect = containerRef.current.getBoundingClientRect();
+ const maxW = rect.width * 0.5;
+ const newW = Math.max(140, Math.min(maxW, e.clientX - rect.left));
+ setTreeWidth(newW);
+ };
+ const onUp = () => { draggingRef.current = false; document.body.style.cursor = ''; };
+ document.addEventListener('mousemove', onMove);
+ document.addEventListener('mouseup', onUp);
+ return () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
+ }, []);
+
+ /* ββ Derived: KPI stats βββββββββββββββββββββββββββββββββββββββββββββββββββ */
+ const kpiStats = useMemo(() => {
+ const cats = CATEGORIES.filter((c) => c !== 'IAM');
+ const catCounts = cats.map((cat) => {
+ const types = EXP_GROUPS[cat];
+ const total = types.reduce((sum, t) => sum + (counts[t.key] || 0), 0);
+ return { cat, total };
+ });
+ catCounts.sort((a, b) => b.total - a.total);
+ const top6 = catCounts.slice(0, 6);
+ const grandTotal = Object.values(counts).reduce((s, v) => s + v, 0);
+ return { top6, grandTotal };
+ }, [counts]);
+
+ /* ββ Derived: filtered resources ββββββββββββββββββββββββββββββββββββββββββ */
+ const filteredResources = useMemo(() => {
+ if (!resourceData) return null;
+ if (!searchFilter.trim()) return resourceData;
+ const q = searchFilter.toLowerCase();
+ return resourceData.filter((r) => {
+ const name = (r.display_name || r.name || '').toLowerCase();
+ const id = (r.id || '').toLowerCase();
+ return name.includes(q) || id.includes(q);
+ });
+ }, [resourceData, searchFilter]);
+
+ /* ββ Find compartment name ββββββββββββββββββββββββββββββββββββββββββββββββ */
+ const selectedCompName = useMemo(() => {
+ const find = (nodes: TreeNode[]): string | null => {
+ for (const n of nodes) {
+ if (n.id === selectedCompartment) return n.name;
+ const c = find(n.children);
+ if (c) return c;
+ }
+ return null;
+ };
+ return find(compartmentTree) || 'β';
+ }, [compartmentTree, selectedCompartment]);
+
+ /* ββ Category resource types with counts ββββββββββββββββββββββββββββββββββ */
+ const currentTypes = EXP_GROUPS[selectedCategory] || [];
+
+ /* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ Render
+ ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+ return (
+
+ {/* ββ Left Panel: Tree ββββββββββββββββββββββββββββββββββββββββββββββββ */}
+
+ {/* Header */}
+
+
+ Compartments
+ {treeLoading && }
+
+
+ {/* Compartment tree */}
+
+ {compartmentTree.length === 0 && !treeLoading && (
+
+ {selectedConfig ? t('exp.noCompartment') : t('exp.selectConfig')}
+
+ )}
+ {compartmentTree.map((node) => (
+
setTreeOpen((p) => ({ ...p, [id]: !p[id] }))}
+ />
+ ))}
+
+
+
+ {/* ββ Resize handle βββββββββββββββββββββββββββββββββββββββββββββββββββ */}
+
{ draggingRef.current = true; document.body.style.cursor = 'col-resize'; }}
+ />
+
+ {/* ββ Right Panel: Content ββββββββββββββββββββββββββββββββββββββββββββ */}
+
+ {/* Toolbar: OCI Config + Regions + Refresh */}
+
+
setSelectedConfig(e.target.value)}
+ >
+ {ociCfg.map((c) => (
+ {c.tenancy_name} ({c.region})
+ ))}
+
+
+
+
+ {selectedRegions.length > 0 && (
+
+ {selectedRegions.map((r) => (
+
+ {r}
+ setSelectedRegions((prev) => prev.filter((x) => x !== r))}
+ >
+ ×
+
+
+ ))}
+
+ )}
+
+
{
+ setCompartmentTree([]);
+ setCounts({});
+ setTreeLoading(true);
+ explorerApi.getCompartmentTree(selectedConfig).then((flat) => {
+ if (!Array.isArray(flat)) return;
+ const tree = buildTree(flat);
+ setCompartmentTree(tree);
+ if (tree.length > 0 && !selectedCompartment) {
+ setSelectedCompartment(tree[0].id);
+ setTreeOpen((p) => ({ ...p, [tree[0].id]: true }));
+ }
+ }).finally(() => setTreeLoading(false));
+ }}
+ >
+ {t('exp.refresh')}
+
+
+
+ {/* KPI Stats Bar */}
+ {selectedCompartment && (
+
+ {/* Compartment info */}
+
+
+ Compartment
+
+
+ {selectedCompName}
+
+
+ {selectedRegions.length || t('exp.all')} {t('exp.regions')}
+
+
+
+ {/* Total */}
+
+
+
+ {kpiStats.grandTotal}
+
+
+ {countsRefreshing ? (
+
+ {t('exp.updating')}
+
+ ) : t('exp.resources')}
+
+
+
+ {/* Category mini KPIs */}
+ {kpiStats.top6.map(({ cat, total }) => {
+ const CIcon = CATEGORY_ICONS[cat] || Package;
+ return (
+
+
+
+
+
{total}
+
{CATEGORY_I18N[cat] ? t(CATEGORY_I18N[cat]) : cat}
+
+ );
+ })}
+
+ )}
+
+ {/* Category Tabs */}
+
+ {CATEGORIES.map((cat) => {
+ const CIcon = CATEGORY_ICONS[cat] || Package;
+ const isSel = selectedCategory === cat;
+ const catTotal = (EXP_GROUPS[cat] || []).reduce((s, t) => s + (counts[t.key] || 0), 0);
+ return (
+ {
+ setSelectedCategory(cat);
+ const types = EXP_GROUPS[cat];
+ if (types.length > 0) setSelectedResourceType(types[0].key);
+ }}
+ >
+
+ {CATEGORY_I18N[cat] ? t(CATEGORY_I18N[cat]) : cat}
+ {catTotal > 0 && (
+
+ {catTotal}
+
+ )}
+
+ );
+ })}
+
+
+ {/* Resource Type Sub-tabs + Search */}
+
+ {currentTypes.map((t) => {
+ const TIcon = ICON_MAP[t.icon] || Package;
+ const isSel = selectedResourceType === t.key;
+ const c = counts[t.key] || 0;
+ return (
+
setSelectedResourceType(t.key)}
+ >
+
+ {t.label}
+ ({c})
+
+ );
+ })}
+
+
+
+ setSearchFilter(e.target.value)}
+ />
+
+
+
+
+ {/* Resource Cards */}
+
+ {loading && (
+
+
+ {t('exp.loadingResources')}
+
+ )}
+
+ {!loading && filteredResources !== null && filteredResources.length === 0 && (
+
+
+
+ {t('exp.noResources')}
+
+ {searchFilter && (
+
+ {t('exp.clearFilter')}
+
+ )}
+
+ )}
+
+ {!loading && filteredResources && filteredResources.length > 0 && (
+
+ {filteredResources.map((item, idx) => (
+ loadResources()}
+ />
+ ))}
+
+ )}
+
+
+
+ );
+}
+
+/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ Resource Card Component
+ ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+function ResourceCard({
+ item, resourceType, configId, actionPending, onAction, regions, onReload,
+}: {
+ item: ResourceItem;
+ resourceType: string;
+ configId: string;
+ actionPending: boolean;
+ onAction: (item: ResourceItem, action: string) => void;
+ regions: string[];
+ onReload: () => void;
+}) {
+ const { t } = useI18n();
+ const name = item.display_name || item.name || item.id || 'β';
+ const state = item.lifecycle_state;
+ const startable = STARTABLE[resourceType];
+
+ let canStart = false;
+ let canStop = false;
+ if (startable && state) {
+ const upper = state.toUpperCase();
+ canStart = startable.stopped.includes(upper);
+ canStop = startable.running.includes(upper);
+ }
+
+ /* Special card body */
+ const specialBody = (() => {
+ switch (resourceType) {
+ case 'instances': return
;
+ case 'databases': return
;
+ case 'db_systems': return
;
+ case 'mysql_db_systems': return
;
+ case 'container_instances': return
;
+ case 'vcns': return
;
+ case 'subnets': return
;
+ case 'buckets': return
;
+ case 'load_balancers': return
;
+ case 'security_lists': return
;
+ case 'nsgs': return
;
+ case 'route_tables': return
;
+ default: return null;
+ }
+ })();
+
+ /* Generic body: show all keys except common ones */
+ const SKIP_KEYS = new Set(['id', 'display_name', 'name', 'lifecycle_state', 'rules', 'listeners',
+ 'backend_sets', 'routes', 'ingress_count', 'egress_count', 'rule_count', 'route_count',
+ 'cidr_blocks', 'dns_label', 'vcn_domain_name', 'approximate_size', 'approximate_count',
+ 'public_access_type', 'versioning', 'storage_tier', 'namespace', 'shape_name', 'ip_addresses',
+ 'is_private', 'cidr_block', 'availability_domain', 'prohibit_public_ip_on_vnic', 'route_table_id',
+ 'security_list_ids', 'subnet_domain_name', 'vcn_id', 'default_route_table_id', 'default_security_list_id',
+ 'db_name', 'whitelisted_ips', 'cpu_core_count', 'data_storage_size_in_tbs', 'is_free_tier',
+ 'database_edition', 'node_count', 'mysql_version', 'is_highly_available', 'container_count',
+ 'shape', 'region', 'time_created', 'application',
+ ]);
+
+ const genericEntries = specialBody ? [] : Object.entries(item).filter(
+ ([k]) => !SKIP_KEYS.has(k)
+ );
+
+ return (
+
+ {/* Header */}
+
+
+
+ {name}
+
+ {item.id && (
+
+ {truncId(item.id)}
+
+ )}
+
+
+ {state && }
+ {actionPending && (
+
+ )}
+
+
+
+ {/* Body */}
+
+ {specialBody || (
+ <>
+ {genericEntries.map(([k, v]) => (
+ c.toUpperCase())}
+ value={
+ typeof v === 'boolean' ? (v ? t('exp.yes') : t('exp.no')) :
+ Array.isArray(v) ? v.join(', ') :
+ typeof v === 'object' && v !== null ? JSON.stringify(v) :
+ String(v ?? 'β')
+ }
+ mono={typeof v === 'string' && (v.startsWith('ocid') || v.includes('.'))}
+ />
+ ))}
+ >
+ )}
+
+
+ {/* Start/Stop Buttons */}
+ {(canStart || canStop) && !actionPending && (
+
+ {canStart && (
+
onAction(item, 'START')}
+ >
+ {t('exp.startAction')}
+
+ )}
+ {canStop && (
+
onAction(item, 'STOP')}
+ >
+ {t('exp.stopAction')}
+
+ )}
+
+ )}
+ {actionPending && (
+
+
+ {t('exp.updatingState')}
+
+ )}
+
+ );
+}
diff --git a/frontend-react/src/pages/LoginPage.tsx b/frontend-react/src/pages/LoginPage.tsx
new file mode 100644
index 0000000..a7d4c9d
--- /dev/null
+++ b/frontend-react/src/pages/LoginPage.tsx
@@ -0,0 +1,128 @@
+import { useState, type FormEvent } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useAuthStore } from '@/stores/auth';
+import { useAppStore } from '@/stores/app';
+import { useI18n } from '@/i18n';
+
+export default function LoginPage() {
+ const navigate = useNavigate();
+ const { t } = useI18n();
+ const { login, mfaRequired, mfaSetup, totpUri, user } = useAuthStore();
+ const { loadData } = useAppStore();
+
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [totp, setTotp] = useState('');
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ if (user) {
+ navigate('/chat', { replace: true });
+ return null;
+ }
+
+ const handleSubmit = async (e: FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setLoading(true);
+ try {
+ const res = await login(username, password, totp || undefined);
+ if (!res.mfa_required && res.token) {
+ await loadData();
+ navigate('/chat', { replace: true });
+ }
+ } catch (err) {
+ setError(err instanceof Error ? err.message : t('login.error'));
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ {/* Logo */}
+
+
+
+
+
+
+
+
+
+
+
AI Agent
+
{t('login.subtitle')}
+
+
+
+
+
+ );
+}
diff --git a/frontend-react/src/pages/PromptGeneratorPage.tsx b/frontend-react/src/pages/PromptGeneratorPage.tsx
new file mode 100644
index 0000000..806276f
--- /dev/null
+++ b/frontend-react/src/pages/PromptGeneratorPage.tsx
@@ -0,0 +1,602 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import {
+ Sparkles, Send, RefreshCw, Copy, Check, PanelLeftOpen,
+ PanelLeftClose, Plus, Trash2, ChevronDown, Loader2, ExternalLink,
+} from 'lucide-react';
+import ReactMarkdown from 'react-markdown';
+import remarkGfm from 'remark-gfm';
+import { useAppStore, type ModelInfo } from '@/stores/app';
+import { terraformApi, type ChatSession } from '@/api/endpoints/terraform';
+import { useI18n } from '@/i18n';
+
+/* ββ Constants ββ */
+
+const TFP_EXAMPLES = [
+ 'VCN com subnets publica e privada, internet gateway, NAT gateway e 2 compute instances Ubuntu',
+ 'Ambiente multi-region MAD1 + MAD3 com DRG, RPC e VCNs espelhadas',
+ 'Cluster OKE com 3 node pools, load balancer e container registry',
+ 'Banco Autonomous Database com vault, encryption key e bastion para acesso',
+ 'Infraestrutura completa: VCN, compute, block storage, object storage, IAM policies e monitoring',
+];
+
+const TFP_ALLOWED = [
+ '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 PROV_ORDER = ['openai', 'google'];
+
+interface Msg {
+ role: 'user' | 'assistant';
+ content: string;
+ raw?: string;
+ failed?: boolean;
+ copied?: boolean;
+}
+
+/* ββ Component ββ */
+
+export default function PromptGeneratorPage() {
+ const { models, genaiCfg, ociCfg } = useAppStore();
+ const { t } = useI18n();
+
+ /* state */
+ const [msgs, setMsgs] = useState
([]);
+ const [loading, setLoading] = useState(false);
+ const [input, setInput] = useState('');
+ const [sid, setSid] = useState(null);
+
+ /* model selection */
+ const [selectedModel, setSelectedModel] = useState('');
+ const [ddOpen, setDdOpen] = useState(false);
+ const [ddSearch, setDdSearch] = useState('');
+
+ /* OCI config (for direct models) */
+ const [ociId, setOciId] = useState('');
+ const [region, setRegion] = useState('');
+ const [compartment, setCompartment] = useState('');
+
+ /* history sidebar */
+ const [histOpen, setHistOpen] = useState(false);
+ const [history, setHistory] = useState([]);
+
+ const chatRef = useRef(null);
+ const inputRef = useRef(null);
+ const ddRef = useRef(null);
+
+ /* ββ auto-scroll ββ */
+ const scrollBottom = useCallback(() => {
+ if (chatRef.current) chatRef.current.scrollTop = chatRef.current.scrollHeight;
+ }, []);
+
+ useEffect(() => { scrollBottom(); }, [msgs, loading, scrollBottom]);
+
+ /* ββ close dropdown on outside click ββ */
+ useEffect(() => {
+ function handleClick(e: MouseEvent) {
+ if (ddRef.current && !ddRef.current.contains(e.target as Node)) setDdOpen(false);
+ }
+ document.addEventListener('mousedown', handleClick);
+ return () => document.removeEventListener('mousedown', handleClick);
+ }, []);
+
+ /* ββ history ββ */
+ const loadHistory = useCallback(async () => {
+ try {
+ const h = await terraformApi.listSessions('tf-prompt');
+ setHistory(h);
+ } catch { setHistory([]); }
+ }, []);
+
+ const loadSession = useCallback(async (sessionId: string) => {
+ try {
+ const d = await terraformApi.loadSession(sessionId);
+ setSid(sessionId);
+ setMsgs(
+ d.messages.map((m) => ({
+ role: m.role,
+ content: m.content,
+ raw: m.role === 'assistant' ? m.content : undefined,
+ })),
+ );
+ } catch { /* noop */ }
+ }, []);
+
+ const deleteSession = useCallback(async (sessionId: string) => {
+ try { await terraformApi.deleteSession(sessionId); } catch { /* noop */ }
+ if (sid === sessionId) { setSid(null); setMsgs([]); }
+ loadHistory();
+ }, [sid, loadHistory]);
+
+ const newConversation = () => { setSid(null); setMsgs([]); };
+
+ /* ββ resolve genai config ββ */
+ const resolveGenai = (): Record => {
+ if (selectedModel.startsWith('cfg:')) return { genai_config_id: selectedModel.slice(4) };
+ if (selectedModel && ociId) return { oci_config_id: ociId, model_id: selectedModel, genai_region: region, compartment_id: compartment };
+ const def = genaiCfg.find((g) => g.is_default) || genaiCfg[0];
+ if (def) return { genai_config_id: def.id };
+ if (!ociCfg.length) return {};
+ const oc = ociCfg[0];
+ return { oci_config_id: oc.id, model_id: 'openai.gpt-4.1', genai_region: oc.region, compartment_id: oc.compartment_id };
+ };
+
+ /* ββ pick model ββ */
+ const pickModel = (v: string) => {
+ setSelectedModel(v);
+ setDdOpen(false);
+ if (v && !v.startsWith('cfg:') && !ociId && ociCfg.length) {
+ setOciId(ociCfg[0].id);
+ setRegion(ociCfg[0].region);
+ setCompartment(ociCfg[0].compartment_id || '');
+ } else if (v.startsWith('cfg:')) {
+ const g = genaiCfg.find((x) => x.id === v.slice(4));
+ if (g?.oci_config_id && !ociId) {
+ setOciId(g.oci_config_id);
+ const c = ociCfg.find((x) => x.id === g.oci_config_id);
+ if (c) { setRegion(c.region); setCompartment(c.compartment_id || ''); }
+ }
+ }
+ };
+
+ /* ββ send ββ */
+ const send = async (overrideMsg?: string) => {
+ const m = overrideMsg || input.trim();
+ if (!m || loading) return;
+ if (!overrideMsg) setInput('');
+
+ const newMsgs: Msg[] = [...msgs, { role: 'user', content: m }];
+ setMsgs(newMsgs);
+ setLoading(true);
+
+ try {
+ const gc = resolveGenai();
+ const hist = newMsgs.slice(0, -1)
+ .filter((x) => x.role === 'user' || x.role === 'assistant')
+ .map((x) => ({ role: x.role === 'user' ? 'USER' : 'CHATBOT', content: x.raw || x.content }));
+
+ const body = {
+ message: m,
+ genai_config: gc,
+ history: hist.length ? hist : null,
+ ...(sid ? { session_id: sid } : {}),
+ };
+
+ const d = await terraformApi.generatePrompt(body);
+ if (d.session_id) setSid(d.session_id);
+
+ if (d.status === 'processing' && d.message_id) {
+ await pollResult(d.message_id, newMsgs);
+ } else if (d.prompt) {
+ setMsgs([...newMsgs, { role: 'assistant', content: d.prompt, raw: d.prompt }]);
+ setLoading(false);
+ }
+ } catch (e: unknown) {
+ setMsgs([...newMsgs, { role: 'assistant', content: 'Erro: ' + (e as Error).message, failed: true }]);
+ setLoading(false);
+ }
+ };
+
+ const pollResult = async (mid: string, currentMsgs: Msg[]) => {
+ for (let i = 0; i < 600; i++) {
+ await new Promise((r) => setTimeout(r, i < 10 ? 1000 : 3000));
+ try {
+ const r = await terraformApi.pollMessageStatus(mid);
+ if (r.status === 'done') {
+ setMsgs([...currentMsgs, { role: 'assistant', content: r.content, raw: r.content }]);
+ setLoading(false);
+ return;
+ }
+ if (r.status === 'failed') {
+ setMsgs([...currentMsgs, { role: 'assistant', content: r.content, failed: true }]);
+ setLoading(false);
+ return;
+ }
+ } catch { /* keep polling */ }
+ }
+ setMsgs([...currentMsgs, { role: 'assistant', content: t('pg.timeout'), failed: true }]);
+ setLoading(false);
+ };
+
+ /* ββ retry ββ */
+ const retry = (idx: number) => {
+ let userMsg = '';
+ for (let i = idx - 1; i >= 0; i--) { if (msgs[i].role === 'user') { userMsg = msgs[i].content; break; } }
+ if (!userMsg) return;
+ const next = [...msgs];
+ next.splice(idx, 1);
+ setMsgs(next);
+ send(userMsg);
+ };
+
+ /* ββ copy ββ */
+ const copyPrompt = async (idx: number) => {
+ const m = msgs[idx];
+ if (!m?.raw) return;
+ await navigator.clipboard.writeText(m.raw);
+ setMsgs((prev) => prev.map((msg, i) => (i === idx ? { ...msg, copied: true } : msg)));
+ setTimeout(() => {
+ setMsgs((prev) => prev.map((msg, i) => (i === idx ? { ...msg, copied: false } : msg)));
+ }, 2000);
+ };
+
+ /* ββ model dropdown data ββ */
+ const provGroups: Record = {};
+ TFP_ALLOWED.forEach((mid) => {
+ const mi: ModelInfo | undefined = models[mid];
+ if (mi) {
+ const p = mi.provider || 'other';
+ if (!provGroups[p]) provGroups[p] = [];
+ provGroups[p].push({ id: mid, name: mi.name });
+ }
+ });
+
+ const cfgFiltered = genaiCfg.filter((g) => TFP_ALLOWED.includes(g.model_id));
+
+ let curLabel = t('pg.selectModel');
+ if (selectedModel.startsWith('cfg:')) {
+ const g = genaiCfg.find((x) => x.id === selectedModel.slice(4));
+ curLabel = g ? (g.name || g.model_id) : 'Config...';
+ } else if (selectedModel) {
+ const mi = models[selectedModel];
+ curLabel = mi ? mi.name : selectedModel;
+ }
+
+ const isDirect = selectedModel && !selectedModel.startsWith('cfg:');
+
+ /* ββ auto-grow textarea ββ */
+ const autoGrow = (el: HTMLTextAreaElement) => {
+ el.style.height = 'auto';
+ el.style.height = Math.min(el.scrollHeight, 200) + 'px';
+ };
+
+ /* ββ history date grouping ββ */
+ const histDateGroup = (ts: string | undefined) => {
+ const tFn = useI18n.getState().t;
+ if (!ts) return '';
+ const dt = ts.slice(0, 10);
+ const today = new Date().toISOString().slice(0, 10);
+ const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
+ if (dt === today) return tFn('chat.today');
+ if (dt === yesterday) return tFn('chat.yesterday');
+ const d = new Date(dt + 'T00:00:00');
+ const diff = Math.floor((Date.now() - d.getTime()) / 86400000);
+ if (diff < 7) return d.toLocaleDateString('pt-BR', { weekday: 'long' });
+ return d.toLocaleDateString('pt-BR', { day: '2-digit', month: 'short', year: 'numeric' });
+ };
+
+ /* ββ render ββ */
+ return (
+
+ {/* ββ History sidebar ββ */}
+ {histOpen && (
+
+
+
{t('pg.history')}
+
+ {t('pg.new')}
+
+
+
+ {history.length === 0 && (
+
+ {t('pg.noConversations')}
+
+ )}
+ {(() => {
+ let lastGroup = '';
+ return history.map((s) => {
+ const g = histDateGroup(s.updated_at || s.created_at);
+ const showGroup = g !== lastGroup;
+ lastGroup = g;
+ return (
+
+ {showGroup && (
+
{g}
+ )}
+
loadSession(s.id)}>
+ {s.title || t('tf.newConversation')}
+ { e.stopPropagation(); deleteSession(s.id); }}
+ title={t('common.delete')}>
+
+
+
+
+ );
+ });
+ })()}
+
+
+ )}
+
+ {/* ββ Main chat area ββ */}
+
+ {/* toolbar */}
+
+
{ setHistOpen(!histOpen); if (!histOpen) loadHistory(); }}
+ className="p-1.5 rounded-md cursor-pointer transition-colors"
+ style={{
+ background: histOpen ? 'var(--ppl)' : 'transparent',
+ color: histOpen ? 'var(--pp)' : 'var(--t3)',
+ border: 'none',
+ }}
+ title={t('pg.history')}>
+ {histOpen ? : }
+
+
+
+
+ {/* Model dropdown */}
+
+
setDdOpen(!ddOpen)}
+ className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-[.72rem] font-medium cursor-pointer"
+ style={{ background: 'var(--bg2)', color: 'var(--t2)', border: '1px solid var(--bd)' }}>
+ {curLabel}
+
+
+
+ {ddOpen && (
+
+
+ setDdSearch(e.target.value)}
+ className="w-full px-2.5 py-1.5 rounded-md text-[.72rem]"
+ style={{ background: 'var(--bg2)', color: 'var(--t1)', border: '1px solid var(--bd)' }}
+ autoFocus />
+
+
+ {cfgFiltered.length > 0 && (
+ <>
+
{t('chat.savedConfigs')}
+ {cfgFiltered
+ .filter((g) => !ddSearch || (g.name || g.model_id).toLowerCase().includes(ddSearch.toLowerCase()))
+ .map((g) => {
+ const mi = models[g.model_id];
+ return (
+
pickModel('cfg:' + g.id)} />
+ );
+ })}
+ >
+ )}
+ {PROV_ORDER.filter((p) => provGroups[p]).map((p) => (
+
+
{p[0].toUpperCase() + p.slice(1)}
+ {provGroups[p]
+ .filter((m) => !ddSearch || m.name.toLowerCase().includes(ddSearch.toLowerCase()))
+ .map((m) => (
+
pickModel(m.id)} />
+ ))}
+
+ ))}
+
+
+ )}
+
+
+ {/* OCI config selector (direct models only) */}
+ {isDirect && (
+
{
+ setOciId(e.target.value);
+ const c = ociCfg.find((x) => x.id === e.target.value);
+ if (c) { setRegion(c.region); setCompartment(c.compartment_id || ''); }
+ }}
+ className="px-2 py-1.5 rounded-lg text-[.72rem] max-w-[180px]"
+ style={{ background: 'var(--bg2)', color: 'var(--t2)', border: '1px solid var(--bd)' }}>
+ {t('tf.ociConfig')}
+ {ociCfg.map((c) => (
+ {c.tenancy_name} ({c.region})
+ ))}
+
+ )}
+
+
+
+
+ {/* messages area */}
+
+ {msgs.length === 0 && !loading && (
+
+
+
+ {t('pg.emptyTitle')}
+
+
+ {t('pg.emptySubtitle')}
+
+
+ {TFP_EXAMPLES.map((ex, i) => (
+ { setInput(ex); inputRef.current?.focus(); }}
+ className="px-3 py-2 rounded-lg text-[.68rem] text-left cursor-pointer transition-colors leading-snug"
+ style={{
+ background: 'var(--bg2)', color: 'var(--t3)',
+ border: '1px solid var(--bd)', maxWidth: 280,
+ }}>
+ {ex}
+
+ ))}
+
+
+ )}
+
+ {msgs.map((m, i) => (
+
+
+
+ {m.role === 'assistant' ? (
+
+
(
+
+
+ {children}
+
+
{children}
+
+ ),
+ code: ({ children, className }) => {
+ if (className?.includes('language-')) return {children};
+ return (
+
+ {children}
+
+ );
+ },
+ }}>
+ {m.content}
+
+
+ ) : (
+
{m.content}
+ )}
+
+
+ {/* assistant actions */}
+ {m.role === 'assistant' && m.raw && !m.failed && (
+
+ copyPrompt(i)}
+ className="flex items-center gap-1 px-2 py-1 rounded-md text-[.64rem] cursor-pointer transition-colors"
+ style={{ background: 'var(--bg3)', color: m.copied ? 'var(--gn)' : 'var(--t3)', border: '1px solid var(--bd)' }}>
+ {m.copied ? : }
+ {m.copied ? t('pg.copied') : t('pg.copy')}
+
+ {
+ const url = `/terraform`;
+ window.open(url, '_blank');
+ }}
+ className="flex items-center gap-1 px-2 py-1 rounded-md text-[.64rem] cursor-pointer transition-colors"
+ style={{ background: 'var(--ppl)', color: 'var(--pp)', border: '1px solid var(--pp)' }}>
+ Terraform
+
+
+ )}
+
+ {/* retry on failure */}
+ {m.failed && (
+
retry(i)}
+ className="flex items-center gap-1 mt-1.5 px-2 py-1 rounded-md text-[.64rem] cursor-pointer"
+ style={{ background: 'var(--rdl)', color: 'var(--rd)', border: '1px solid var(--rd)' }}>
+ {t('pg.retry')}
+
+ )}
+
+
+ ))}
+
+ {/* loading indicator */}
+ {loading && (
+
+
+
+ {t('pg.loading')}
+
+
+ )}
+
+
+ {/* input area */}
+
+
+
+ );
+}
+
+/* ββ Copy code block button ββ */
+
+function CopyCodeBtn({ children }: { children: React.ReactNode }) {
+ const extractText = (node: React.ReactNode): string => {
+ if (typeof node === 'string') return node;
+ if (Array.isArray(node)) return node.map(extractText).join('');
+ if (node && typeof node === 'object' && 'props' in node) {
+ return extractText((node as React.ReactElement<{ children?: React.ReactNode }>).props.children);
+ }
+ return '';
+ };
+
+ return (
+ navigator.clipboard.writeText(extractText(children))}
+ title={useI18n.getState().t('common.copy')}>
+
+
+ );
+}
+
+/* ββ Dropdown item ββ */
+
+function DdItem({ label, selected, onClick }: { label: string; selected: boolean; onClick: () => void }) {
+ return (
+
+ {label}
+
+ );
+}
diff --git a/frontend-react/src/pages/ReportsPage.tsx b/frontend-react/src/pages/ReportsPage.tsx
new file mode 100644
index 0000000..5c63333
--- /dev/null
+++ b/frontend-react/src/pages/ReportsPage.tsx
@@ -0,0 +1,1629 @@
+import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
+import { useAppStore } from '@/stores/app';
+import { useI18n } from '@/i18n';
+import { usePolling } from '@/hooks/usePolling';
+import {
+ reportsApi,
+ type Report,
+ type ReportProgress,
+ type ReportSummary,
+ type ReportFile,
+} from '@/api/endpoints/reports';
+import {
+ BarChart3, RefreshCw, Play, XCircle, ChevronDown, ChevronRight,
+ Search, X, Loader2, AlertCircle, CheckCircle2, Clock, FileText,
+ Download, ShieldCheck, ShieldAlert, Activity, Eye, EyeOff, Trash2,
+ Database, Lock, Globe, Server, Package, Dna, Check,
+} from 'lucide-react';
+import {
+ PieChart, Pie, Cell, BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer,
+} from 'recharts';
+
+/* ββ Fallback Regions ββ */
+const FALLBACK_REGIONS: Record = {
+ 'Americas': [
+ 'us-ashburn-1', 'us-phoenix-1', 'us-sanjose-1', 'us-chicago-1',
+ 'ca-toronto-1', 'ca-montreal-1', 'sa-saopaulo-1', 'sa-vinhedo-1',
+ 'sa-santiago-1', 'sa-bogota-1', 'mx-queretaro-1', 'mx-monterrey-1',
+ ],
+ 'Europe': [
+ 'eu-frankfurt-1', 'eu-amsterdam-1', 'eu-zurich-1', 'eu-madrid-1',
+ 'eu-marseille-1', 'eu-milan-1', 'eu-stockholm-1', 'eu-paris-1',
+ 'uk-london-1', 'uk-cardiff-1', 'eu-jovanovac-1',
+ ],
+ 'Asia Pacific': [
+ 'ap-tokyo-1', 'ap-osaka-1', 'ap-seoul-1', 'ap-sydney-1',
+ 'ap-melbourne-1', 'ap-mumbai-1', 'ap-hyderabad-1', 'ap-singapore-1',
+ ],
+ 'Middle East & Africa': [
+ 'me-jeddah-1', 'me-dubai-1', 'me-abudhabi-1',
+ 'af-johannesburg-1', 'il-jerusalem-1',
+ ],
+};
+
+/* ββ Progress estimation ββ */
+function estimateProgress(lines: string[]): number {
+ if (!lines.length) return 0;
+ const last = lines[lines.length - 1].toLowerCase();
+ // Walk backwards to find the most advanced keyword
+ for (let i = lines.length - 1; i >= 0; i--) {
+ const l = lines[i].toLowerCase();
+ if (l.includes('finished')) return 100;
+ if (l.includes('best practices')) return 85;
+ if (l.includes('writing cis reports') || l.includes('writing reports')) return 75;
+ if (l.includes('cis summary report') || l.includes('summary report')) return 60;
+ if (l.includes('processing regional')) return 35;
+ if (l.includes('processing home region')) return 20;
+ if (l.includes('identity domains') || l.includes('identity')) return 10;
+ }
+ // If there are lines but no keyword matched, at least show minimal progress
+ if (last) return 5;
+ return 0;
+}
+
+function scoreColor(score: number): string {
+ if (score >= 80) return 'var(--gn)';
+ if (score >= 60) return '#f59e0b';
+ return 'var(--rd)';
+}
+
+function statusBadge(status: string) {
+ const t = useI18n.getState().t;
+ const map: Record = {
+ running: { bg: 'color-mix(in srgb, var(--bl) 12%, transparent)', fg: 'var(--bl)', label: t('rpt.inExecution') },
+ completed: { bg: 'var(--gnl)', fg: 'var(--gn)', label: t('rpt.completed') },
+ failed: { bg: 'var(--rdl)', fg: 'var(--rd)', label: t('rpt.failed') },
+ cancelled: { bg: 'color-mix(in srgb, var(--t4) 12%, transparent)', fg: 'var(--t4)', label: t('rpt.cancelled') },
+ };
+ const s = map[status] || { bg: 'var(--bg2)', fg: 'var(--t4)', label: status };
+ return (
+
+ {status === 'running' && }
+ {status === 'completed' && }
+ {status === 'failed' && }
+ {status === 'cancelled' && }
+ {s.label}
+
+ );
+}
+
+/* ββ File Grouping Helpers ββ */
+
+function extractSection(name: string): string {
+ const m = name.match(/^(?:cis|obp)_([A-Za-z_]+?)_\d/);
+ if (m) return m[1].replace(/_/g, ' ');
+ if (name.includes('summary')) return 'Summary';
+ if (name.includes('error')) return 'Error';
+ return 'Other';
+}
+
+function formatFileSize(bytes: number): string {
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
+}
+
+function fileExt(name: string): string {
+ return (name.split('.').pop() || '').toLowerCase();
+}
+
+const extColors: Record = {
+ html: '#e44d26', csv: '#217346', json: '#f5a623',
+ xlsx: '#217346', txt: '#6b7280', pdf: '#dc2626',
+};
+
+const sectionIcons: Record = {
+ 'Identity and Access Management': Lock,
+ 'Networking': Globe,
+ 'Compute': Server,
+ 'Logging and Monitoring': Activity,
+ 'Storage Object Storage': Database,
+ 'Storage Block Volumes': Database,
+ 'Storage File Storage Service': Database,
+ 'Asset Management': Package,
+ 'Summary': BarChart3,
+ 'Error': AlertCircle,
+ 'Other': FileText,
+};
+
+function sectionSort(a: string, b: string): number {
+ if (a === 'Summary') return -1;
+ if (b === 'Summary') return 1;
+ if (a === 'Other') return 1;
+ if (b === 'Other') return -1;
+ return a.localeCompare(b);
+}
+
+function groupBySection(files: ReportFile[]): Record {
+ const map: Record = {};
+ for (const f of files) {
+ const sec = extractSection(f.file_name);
+ (map[sec] ||= []).push(f);
+ }
+ return map;
+}
+
+/* ββ SVG Gauge ββ */
+function ComplianceGauge({ score }: { score: number }) {
+ const radius = 54;
+ const stroke = 10;
+ const circumference = 2 * Math.PI * radius;
+ const pct = Math.min(Math.max(score, 0), 100);
+ const offset = circumference - (pct / 100) * circumference;
+ const color = scoreColor(pct);
+
+ return (
+
+
+
+
+ {pct.toFixed(1)}%
+
+
+ Compliance
+
+
+ );
+}
+
+/* ββ Region Multi-Select ββ */
+function RegionMultiSelect({
+ selected,
+ onChange,
+ regionsMap,
+}: {
+ selected: string[];
+ onChange: (regions: string[]) => void;
+ regionsMap: Record;
+}) {
+ const { t } = useI18n();
+ const [open, setOpen] = useState(false);
+ const [filter, setFilter] = useState('');
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const handler = (e: MouseEvent) => {
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
+ };
+ document.addEventListener('mousedown', handler);
+ return () => document.removeEventListener('mousedown', handler);
+ }, []);
+
+ const f = filter.toLowerCase();
+
+ const toggle = (r: string) => {
+ if (selected.includes(r)) {
+ onChange(selected.filter((s) => s !== r));
+ } else {
+ onChange([...selected, r]);
+ }
+ };
+
+ const removeTag = (r: string) => onChange(selected.filter((s) => s !== r));
+
+ return (
+
+
setOpen(!open)}
+ className="flex items-center flex-wrap gap-1.5 cursor-pointer px-3 py-2 rounded-lg min-h-[36px]"
+ style={{ background: 'var(--bg2)', border: '1.5px solid var(--bd)' }}
+ >
+ {selected.length === 0 ? (
+
+ {t('rpt.allRegionsDefault')}
+
+ ) : (
+ selected.map((r) => (
+
+ {r}
+ { e.stopPropagation(); removeTag(r); }}
+ />
+
+ ))
+ )}
+
+
+
+ {open && (
+
+
+
+
+ setFilter(e.target.value)}
+ onClick={(e) => e.stopPropagation()}
+ className="w-full border-none outline-none text-xs py-1.5"
+ style={{ background: 'transparent', color: 'var(--t1)' }}
+ autoFocus
+ />
+
+
+
+ {Object.entries(regionsMap).map(([grp, regs]) => {
+ const filtered = regs.filter((r) => !f || r.includes(f));
+ if (!filtered.length) return null;
+ return (
+
+
+ {grp}
+
+ {filtered.map((r) => {
+ const isSelected = selected.includes(r);
+ return (
+
toggle(r)}
+ className="flex items-center gap-2 px-3 py-1.5 text-xs cursor-pointer transition-colors"
+ style={{
+ color: isSelected ? 'var(--ac)' : 'var(--t2)',
+ background: isSelected ? 'color-mix(in srgb, var(--ac) 8%, transparent)' : undefined,
+ }}
+ onMouseEnter={(e) => { if (!isSelected) (e.currentTarget.style.background = 'var(--bg2)'); }}
+ onMouseLeave={(e) => { if (!isSelected) (e.currentTarget.style.background = ''); }}
+ >
+
+ {isSelected && }
+
+ {r}
+
+ );
+ })}
+
+ );
+ })}
+
+
+ )}
+
+ );
+}
+
+/* ββ OCI Config Dropdown ββ */
+function ConfigDropdown({
+ configs,
+ value,
+ onChange,
+}: {
+ configs: { id: string; tenancy_name: string; region: string }[];
+ value: string;
+ onChange: (id: string) => void;
+}) {
+ const { t } = useI18n();
+ const [open, setOpen] = useState(false);
+ const [filter, setFilter] = useState('');
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const handler = (e: MouseEvent) => {
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
+ };
+ document.addEventListener('mousedown', handler);
+ return () => document.removeEventListener('mousedown', handler);
+ }, []);
+
+ const f = filter.toLowerCase();
+ const selected = configs.find((c) => c.id === value);
+ const filtered = configs.filter(
+ (c) => !f || c.tenancy_name.toLowerCase().includes(f) || c.region.toLowerCase().includes(f),
+ );
+
+ return (
+
+
setOpen(!open)}
+ className="flex items-center justify-between cursor-pointer px-3 py-2 rounded-lg text-[.78rem]"
+ style={{ background: 'var(--bg2)', border: '1.5px solid var(--bd)', minHeight: 36 }}
+ >
+
+ {selected ? `${selected.tenancy_name} (${selected.region})` : t('rpt.selectOciCred')}
+
+
+
+
+ {open && (
+
+
+
+
+ setFilter(e.target.value)}
+ onClick={(e) => e.stopPropagation()}
+ className="w-full border-none outline-none text-xs py-1.5"
+ style={{ background: 'transparent', color: 'var(--t1)' }}
+ autoFocus
+ />
+
+
+
+ {filtered.length === 0 ? (
+
{t('rpt.noCredentials')}
+ ) : (
+ filtered.map((c) => (
+
{ onChange(c.id); setOpen(false); setFilter(''); }}
+ className="px-3 py-2 text-xs cursor-pointer transition-colors"
+ style={{
+ color: value === c.id ? 'var(--ac)' : 'var(--t2)',
+ background: value === c.id ? 'color-mix(in srgb, var(--ac) 8%, transparent)' : undefined,
+ }}
+ onMouseEnter={(e) => { if (value !== c.id) (e.currentTarget.style.background = 'var(--bg2)'); }}
+ onMouseLeave={(e) => { if (value !== c.id) (e.currentTarget.style.background = ''); }}
+ >
+
{c.tenancy_name}
+
{c.region}
+
+ ))
+ )}
+
+
+ )}
+
+ );
+}
+
+/* ββ Report Selector ββ */
+function ReportSelector({
+ reports,
+ value,
+ onChange,
+}: {
+ reports: Report[];
+ value: string;
+ onChange: (id: string) => void;
+}) {
+ const { t } = useI18n();
+ const [open, setOpen] = useState(false);
+ const [filter, setFilter] = useState('');
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const handler = (e: MouseEvent) => {
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
+ };
+ document.addEventListener('mousedown', handler);
+ return () => document.removeEventListener('mousedown', handler);
+ }, []);
+
+ const f = filter.toLowerCase();
+ const completed = reports.filter((r) => r.status === 'completed');
+ const selected = completed.find((r) => r.id === value);
+ const filtered = completed.filter(
+ (c) => !f || c.tenancy_name.toLowerCase().includes(f) || c.created_at.toLowerCase().includes(f),
+ );
+
+ return (
+
+
setOpen(!open)}
+ className="flex items-center justify-between cursor-pointer px-3 py-2 rounded-lg text-[.78rem]"
+ style={{ background: 'var(--bg2)', border: '1.5px solid var(--bd)', minHeight: 36 }}
+ >
+
+ {selected
+ ? `${selected.tenancy_name} β Level ${selected.level || 2} β ${selected.created_at}`
+ : t('rpt.selectCompletedReport')}
+
+
+
+
+ {open && (
+
+
+
+
+ setFilter(e.target.value)}
+ onClick={(e) => e.stopPropagation()}
+ className="w-full border-none outline-none text-xs py-1.5"
+ style={{ background: 'transparent', color: 'var(--t1)' }}
+ autoFocus
+ />
+
+
+
+ {filtered.length === 0 ? (
+
{t('rpt.noReports')}
+ ) : (
+ filtered.map((r) => (
+
{ onChange(r.id); setOpen(false); setFilter(''); }}
+ className="px-3 py-2 text-xs cursor-pointer transition-colors"
+ style={{
+ color: value === r.id ? 'var(--ac)' : 'var(--t2)',
+ background: value === r.id ? 'color-mix(in srgb, var(--ac) 8%, transparent)' : undefined,
+ }}
+ onMouseEnter={(e) => { if (value !== r.id) (e.currentTarget.style.background = 'var(--bg2)'); }}
+ onMouseLeave={(e) => { if (value !== r.id) (e.currentTarget.style.background = ''); }}
+ >
+
{r.tenancy_name}
+
+ Level {r.level || 2}
+ |
+ {r.created_at}
+
+
+ ))
+ )}
+
+
+ )}
+
+ );
+}
+
+/* ββ Recharts Custom Tooltip ββ */
+function CustomTooltip({ active, payload, label }: { active?: boolean; payload?: { value: number; name: string; fill?: string }[]; label?: string }) {
+ if (!active || !payload?.length) return null;
+ return (
+
+ {label &&
{label}
}
+ {payload.map((p, i) => (
+
+
+
{p.name}: {p.value}
+
+ ))}
+
+ );
+}
+
+/* ββ Main Component ββ */
+
+export default function ReportsPage() {
+ const { t } = useI18n();
+ const ociCfg = useAppStore((s) => s.ociCfg);
+ const ociRegions = useAppStore((s) => s.ociRegions);
+ const adbCfg = useAppStore((s) => s.adbCfg);
+
+ /* ββ Reports list ββ */
+ const [reports, setReports] = useState([]);
+ const [reportsLoading, setReportsLoading] = useState(true);
+
+ /* ββ Form state ββ */
+ const [formOpen, setFormOpen] = useState(true);
+ const [ociVal, setOciVal] = useState('');
+ const [level, setLevel] = useState<1 | 2>(2);
+ const [selectedRegions, setSelectedRegions] = useState([]);
+ const [obp, setObp] = useState(true);
+ const [raw, setRaw] = useState(false);
+ const [redact, setRedact] = useState(true);
+
+ /* ββ Tracking / progress ββ */
+ const [trackingId, setTrackingId] = useState(null);
+ const [progress, setProgress] = useState(null);
+ const [logExpanded, setLogExpanded] = useState(false);
+
+ /* ββ Selected report + KPI ββ */
+ const [selectedRid, setSelectedRid] = useState('');
+ const [kpi, setKpi] = useState(null);
+ const [kpiLoading, setKpiLoading] = useState(false);
+
+ /* ββ Report files ββ */
+ const [files, setFiles] = useState([]);
+
+ /* ββ Embedding state ββ */
+ const [embedAdb, setEmbedAdb] = useState('');
+ const [embedTable, setEmbedTable] = useState('');
+ const [embedLoading, setEmbedLoading] = useState(false);
+ const [embedMsg, setEmbedMsg] = useState<{ type: 's' | 'e'; text: string } | null>(null);
+
+ /* ββ HTML iframe ββ */
+ const [showIframe, setShowIframe] = useState(false);
+ const [showCompliance, setShowCompliance] = useState(false);
+ const [complianceReady, setComplianceReady] = useState(false);
+ const [complianceGenerating, setComplianceGenerating] = useState(false);
+
+ /* ββ Error ββ */
+ const [error, setError] = useState('');
+
+ /* ββ Regions map ββ */
+ const regionsMap = useMemo>(() => {
+ if (ociRegions && typeof ociRegions === 'object') {
+ const entries = Object.entries(ociRegions);
+ if (entries.length > 0) {
+ const first = entries[0][1];
+ if (Array.isArray(first)) return ociRegions as unknown as Record;
+ const flat = entries.map(([, v]) => v as unknown as string);
+ if (flat.length > 0) return { [t('rpt.regions')]: flat };
+ }
+ }
+ return FALLBACK_REGIONS;
+ }, [ociRegions]);
+
+ /* ββ Init embed ADB ββ */
+ useEffect(() => {
+ if (!embedAdb && adbCfg.length) {
+ const active = adbCfg.find((c) => c.is_active);
+ const cfg = active || adbCfg[0];
+ setEmbedAdb(cfg.id);
+ const tables = (cfg.tables || []).filter((t) => t.is_active);
+ setEmbedTable(tables[0]?.table_name || '');
+ }
+ }, [adbCfg, embedAdb]);
+
+ /* ββ Update table when ADB changes ββ */
+ const embedAdbTables = useMemo(() => {
+ const cfg = adbCfg.find((c) => c.id === embedAdb);
+ return (cfg?.tables || []).filter((t) => t.is_active);
+ }, [adbCfg, embedAdb]);
+
+ /* ββ Load reports ββ */
+ const loadReports = useCallback(async () => {
+ setReportsLoading(true);
+ setError('');
+ try {
+ const data = await reportsApi.list();
+ setReports(data);
+ // Auto-track if there's a running report
+ const running = data.find((r) => r.status === 'running');
+ if (running && !trackingId) {
+ setTrackingId(running.id);
+ }
+ } catch (err) {
+ setError(err instanceof Error ? err.message : t('rpt.errorLoadReports'));
+ } finally {
+ setReportsLoading(false);
+ }
+ }, [trackingId]);
+
+ useEffect(() => { loadReports(); }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ /* ββ Poll progress ββ */
+ usePolling(
+ async () => {
+ if (!trackingId) return true;
+ try {
+ const p = await reportsApi.progress(trackingId);
+ setProgress(p);
+ if (p.status === 'completed' || p.status === 'failed' || p.status === 'cancelled') {
+ // Refresh reports list
+ await loadReports();
+ if (p.status === 'completed') {
+ setSelectedRid(trackingId);
+ }
+ setTrackingId(null);
+ return true; // stop polling
+ }
+ } catch {
+ // Continue polling on transient errors
+ }
+ return false;
+ },
+ 3000,
+ !!trackingId,
+ );
+
+ /* ββ Load KPI when selected report changes ββ */
+ useEffect(() => {
+ if (!selectedRid) {
+ setKpi(null);
+ setFiles([]);
+ return;
+ }
+ let cancelled = false;
+ setKpiLoading(true);
+ Promise.all([
+ reportsApi.summary(selectedRid),
+ reportsApi.files(selectedRid),
+ reportsApi.complianceReportStatus(selectedRid).catch(() => ({ ready: false })),
+ ])
+ .then(([summaryData, filesData, compStatus]) => {
+ if (!cancelled) {
+ setKpi(summaryData);
+ setFiles(filesData);
+ setComplianceReady(compStatus.ready);
+ setComplianceGenerating(false);
+ }
+ })
+ .catch(() => {
+ if (!cancelled) {
+ setKpi(null);
+ setFiles([]);
+ setComplianceReady(false);
+ }
+ })
+ .finally(() => {
+ if (!cancelled) setKpiLoading(false);
+ });
+ return () => { cancelled = true; };
+ }, [selectedRid]);
+
+ /* ββ Handlers ββ */
+ const handleRun = async () => {
+ if (!ociVal) {
+ setError(t('rpt.selectOciCredError'));
+ return;
+ }
+ if (trackingId) {
+ setError(t('rpt.alreadyRunning'));
+ return;
+ }
+ setError('');
+ setProgress(null);
+ setLogExpanded(false);
+ try {
+ const result = await reportsApi.run({
+ config_id: ociVal,
+ regions: selectedRegions.length > 0 ? selectedRegions : undefined,
+ level,
+ obp,
+ raw,
+ redact_output: redact,
+ });
+ setTrackingId(result.report_id);
+ setFormOpen(false);
+ await loadReports();
+ } catch (err) {
+ setError(err instanceof Error ? err.message : t('rpt.errorStartScan'));
+ }
+ };
+
+ const handleCancel = async () => {
+ if (!trackingId) return;
+ try {
+ await reportsApi.cancel(trackingId);
+ } catch {
+ // ignore
+ }
+ };
+
+ const handleEmbed = async (rid: string) => {
+ if (!embedAdb) {
+ setEmbedMsg({ type: 'e', text: t('rpt.selectAdb') });
+ return;
+ }
+ if (!embedTable) {
+ setEmbedMsg({ type: 'e', text: t('rpt.selectTable') });
+ return;
+ }
+ setEmbedLoading(true);
+ setEmbedMsg(null);
+ try {
+ const r = await reportsApi.embedReport(rid, embedAdb, embedTable);
+ setEmbedMsg({ type: 's', text: r.message });
+ } catch (err) {
+ setEmbedMsg({ type: 'e', text: err instanceof Error ? err.message : t('rpt.errorEmbedding') });
+ } finally {
+ setEmbedLoading(false);
+ }
+ };
+
+ const handleEmbedFile = async (rid: string, fid: string) => {
+ if (!embedAdb) {
+ setEmbedMsg({ type: 'e', text: t('rpt.selectAdb') });
+ return;
+ }
+ if (!embedTable) {
+ setEmbedMsg({ type: 'e', text: t('rpt.selectTable') });
+ return;
+ }
+ setEmbedLoading(true);
+ setEmbedMsg(null);
+ try {
+ const r = await reportsApi.embedFile(rid, fid, embedAdb, embedTable);
+ setEmbedMsg({ type: 's', text: r.message });
+ } catch (err) {
+ setEmbedMsg({ type: 'e', text: err instanceof Error ? err.message : t('rpt.errorEmbedding') });
+ } finally {
+ setEmbedLoading(false);
+ }
+ };
+
+ const handleRefresh = () => {
+ loadReports();
+ };
+
+ /* ββ Derived data ββ */
+ const isRunning = !!trackingId;
+ const pct = progress ? estimateProgress(progress.progress) : 0;
+ const lastLine = progress?.progress?.length ? progress.progress[progress.progress.length - 1] : '';
+
+ /* ββ KPI chart data ββ */
+ const pieData = kpi
+ ? [
+ { name: t('rpt.compliant'), value: kpi.passed, fill: 'var(--gn)' },
+ { name: t('rpt.nonCompliant'), value: kpi.failed, fill: 'var(--rd)' },
+ ]
+ : [];
+
+ const barData = kpi
+ ? Object.entries(kpi.sections)
+ .map(([name, s]) => ({ name: name.length > 25 ? name.slice(0, 25) + '...' : name, fullName: name, failed: s.failed, passed: s.passed }))
+ .sort((a, b) => b.failed - a.failed)
+ : [];
+
+ /* ββ Style helpers ββ */
+ const cardStyle = { background: 'var(--bg1)', border: '1px solid var(--bd)' };
+ const labelCls = "block text-[.72rem] font-semibold mb-1";
+ const labelStyle = { color: 'var(--t3)' };
+
+ return (
+
+
+
+ {/* βββ Header βββ */}
+
+
+
+
+
+
+
{t('rpt.pageTitle')}
+
+ {t('rpt.pageSubtitle')}
+
+
+
+
+
+ {t('rpt.refresh')}
+
+
+
+ {/* βββ Error βββ */}
+ {error && (
+
+
+ {error}
+
setError('')}
+ />
+
+ )}
+
+ {/* βββ Run Report Form (collapsible) βββ */}
+
+
setFormOpen(!formOpen)}
+ >
+
+
+
+ {t('rpt.runScanCis')}
+
+ {isRunning && (
+
+
+ {t('rpt.inExecution')}
+
+ )}
+
+
+ {formOpen && (
+
+
+ {t('rpt.configParams')}
+
+
+ {/* Row 1: Config + Level */}
+
+
+ {t('rpt.ociCredential')}
+
+
+
+
{t('rpt.cisLevel')}
+
+ {([1, 2] as const).map((l) => (
+ setLevel(l)}
+ className="flex-1 flex items-center justify-center gap-1.5 px-3 py-2 rounded-lg text-[.78rem] font-semibold transition-all"
+ style={{
+ background: level === l ? 'color-mix(in srgb, var(--ac) 15%, transparent)' : 'var(--bg2)',
+ color: level === l ? 'var(--ac)' : 'var(--t4)',
+ border: `1.5px solid ${level === l ? 'var(--ac)' : 'var(--bd)'}`,
+ }}
+ >
+ Level {l}
+
+ ))}
+
+
+ {level === 1 ? t('rpt.level1Desc') : t('rpt.level2Desc')}
+
+
+
+
+ {/* Row 2: Regions */}
+
+
{t('rpt.regions')}
+
+
+ {t('rpt.leaveEmpty')}
+
+
+
+ {/* Row 3: Checkboxes */}
+
+ {[
+ { id: 'obp', label: 'OCI Best Practices', checked: obp, set: setObp },
+ { id: 'raw', label: 'Raw Data', checked: raw, set: setRaw },
+ { id: 'redact', label: 'Redact OCIDs', checked: redact, set: setRedact },
+ ].map(({ id, label, checked, set }) => (
+
+ set(!checked)}
+ >
+ {checked && }
+
+ {label}
+
+ ))}
+
+
+ {/* Execute button */}
+
+
+ {isRunning ? (
+
+ ) : (
+
+ )}
+ {t('rpt.executeCompliance')}
+
+
+
+ )}
+
+
+ {/* βββ Progress Card βββ */}
+ {(isRunning || (progress && progress.status !== 'completed')) && progress && (
+
+
+
+
+ {t('rpt.scanProgress')}
+
+
+ {pct}%
+
+
+
+
+ {/* Progress bar */}
+
+
+ {/* Last status line */}
+ {lastLine && (
+
+
+
+ {lastLine}
+
+
+ )}
+
+ {/* Collapsible log */}
+
+
setLogExpanded(!logExpanded)}
+ className="flex items-center gap-1 text-[.68rem] font-medium"
+ style={{ color: 'var(--t4)' }}
+ >
+
+ {logExpanded ? t('rpt.hideLog') : t('rpt.showLog')} ({progress.progress.length} {t('rpt.lines')})
+
+ {logExpanded && (
+
+ {progress.progress.map((line, i) => (
+
{line}
+ ))}
+
+ )}
+
+
+ {/* Cancel button */}
+ {isRunning && (
+
+
+ {t('rpt.cancelScan')}
+
+ )}
+
+ {/* Error message for failed */}
+ {progress.status === 'failed' && progress.error_msg && (
+
+
+ {progress.error_msg}
+
+ )}
+
+
+ )}
+
+ {/* βββ Report Selector βββ */}
+
+
+
+
+ {t('rpt.selectReport')}
+
+
+
+
+
+
+
+ {/* βββ KPI Dashboard βββ */}
+ {kpiLoading && (
+
+
+ {t('rpt.loadingDashboard')}
+
+ )}
+
+ {kpi && !kpiLoading && (
+ <>
+ {/* KPI Cards */}
+
+ {/* Score Gauge */}
+
+
+
+ {t('rpt.complianceScore')}
+
+
+
+ {/* Compliant */}
+
+
+
+ {t('rpt.compliant')}
+
+
+ {kpi.passed}
+
+
+ {kpi.total > 0 ? ((kpi.passed / kpi.total) * 100).toFixed(1) : 0}% {t('rpt.ofTotal')}
+
+
+ {kpi.total_compliant} {t('rpt.compliantItems')}
+
+
+
+ {/* Non-Compliant */}
+
+
+
+ {t('rpt.nonCompliant')}
+
+
+ {kpi.failed}
+
+
+ {kpi.total_findings} {t('rpt.totalFindings')}
+
+
+
+ {/* Total Evaluated */}
+
+
+
+
{t('rpt.evaluated')}
+
+
+ {kpi.total}
+
+
+ {t('rpt.controlsVerified')}
+
+
+ Level {kpi.level} | {kpi.regions.length} {t('rpt.regionsCount')}
+
+
+
+
+ {/* Charts Row */}
+
+ {/* Donut Chart */}
+
+
+ {t('rpt.complianceDist')}
+
+
+
+
+
+ {pieData.map((entry, i) => (
+ |
+ ))}
+
+ } />
+
+
+
+
+
+
+
{t('rpt.compliant')} ({kpi.passed})
+
+
+
+
{t('rpt.nonCompliant')} ({kpi.failed})
+
+
+
+
+ {/* Horizontal Bar Chart */}
+
+
+ {t('rpt.findingsBySection')}
+
+
+
+
+
+
+ } />
+
+
+
+
+
+
+
+
+ {/* Summary info */}
+
+ {t('rpt.tenancy')}: {kpi.tenancy}
+ {t('rpt.level')}: {kpi.level}
+ {t('rpt.regions')}: {kpi.regions.join(', ')}
+ {t('rpt.generatedAt')}: {kpi.generated_at}
+
+ >
+ )}
+
+ {/* βββ HTML Report Iframe βββ */}
+ {selectedRid && (
+
+
setShowIframe(!showIframe)}
+ >
+
+ {showIframe ? : }
+
+ {t('rpt.htmlReport')}
+
+
+
+
+ {showIframe && (
+
+ )}
+
+ )}
+
+ {/* βββ LAD A-Team CIS Compliance Report βββ */}
+ {selectedRid && (
+
+
setShowCompliance(!showCompliance)}
+ >
+
+ {showCompliance ? : }
+
+ LAD A-Team CIS Compliance Report
+
+ {complianceReady && (
+
+ + Remediation
+
+ )}
+ {complianceGenerating && (
+
+ Gerando...
+
+ )}
+
+
+
+ {showCompliance && (
+ complianceReady ? (
+
+
+
+ {
+ setComplianceGenerating(true);
+ setComplianceReady(false);
+ try {
+ await reportsApi.generateComplianceReport(selectedRid);
+ } catch { /* ignore */ }
+ const poll = setInterval(async () => {
+ try {
+ const s = await reportsApi.complianceReportStatus(selectedRid);
+ if (s.ready) { clearInterval(poll); setComplianceReady(true); setComplianceGenerating(false); }
+ } catch { /* keep polling */ }
+ }, 3000);
+ setTimeout(() => { clearInterval(poll); setComplianceGenerating(false); }, 300000);
+ }}
+ disabled={complianceGenerating}
+ className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[.68rem] font-medium cursor-pointer transition-colors"
+ style={{ background: 'var(--bg3)', color: 'var(--t2)', border: '1px solid var(--bd)' }}
+ >
+ Regenerar
+
+
+
+ ) : complianceGenerating ? (
+
+
+
Gerando Compliance Report...
+
Buscando remediaΓ§Γ΅es na Knowledge Base CIS
+
+ ) : (
+
+
+
Report ainda nΓ£o gerado
+
{
+ setComplianceGenerating(true);
+ try {
+ await reportsApi.generateComplianceReport(selectedRid);
+ } catch { /* ignore */ }
+ const poll = setInterval(async () => {
+ try {
+ const s = await reportsApi.complianceReportStatus(selectedRid);
+ if (s.ready) { clearInterval(poll); setComplianceReady(true); setComplianceGenerating(false); }
+ } catch { /* keep polling */ }
+ }, 3000);
+ setTimeout(() => { clearInterval(poll); setComplianceGenerating(false); }, 300000);
+ }}
+ className="flex items-center gap-2 px-4 py-2 rounded-lg text-[.76rem] font-semibold cursor-pointer"
+ style={{ background: 'var(--gn)', color: '#fff' }}
+ >
+ Gerar Compliance Report
+
+
+ )
+ )}
+
+ )}
+
+ {/* βββ Report Files (grouped by section) βββ */}
+ {selectedRid && files.length > 0 && (() => {
+ const grouped = groupBySection(files);
+ const sections = Object.keys(grouped).sort(sectionSort);
+ const token = localStorage.getItem('t') || '';
+
+ return (
+
+ {/* Header with embedding controls */}
+
+
+
+
+ {t('rpt.files')} ({files.length})
+
+
+ {sections.length} {t('rpt.inSections')}
+
+
+
+ {/* Embedding controls */}
+ {adbCfg.length > 0 && (
+
+ {
+ setEmbedAdb(e.target.value);
+ const cfg = adbCfg.find((c) => c.id === e.target.value);
+ const tables = (cfg?.tables || []).filter((t) => t.is_active);
+ setEmbedTable(tables[0]?.table_name || '');
+ }}
+ className="text-[.68rem] py-1 px-2 rounded-md outline-none cursor-pointer"
+ style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t1)', maxWidth: 160 }}
+ >
+ {adbCfg.map((c) => (
+ {c.config_name}
+ ))}
+
+ setEmbedTable(e.target.value)}
+ className="text-[.68rem] py-1 px-2 rounded-md outline-none cursor-pointer"
+ style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t1)', maxWidth: 180 }}
+ >
+ {embedAdbTables.length === 0 && {t('rpt.noActiveTables')} }
+ {embedAdbTables.map((t) => (
+ {t.table_name}
+ ))}
+
+ handleEmbed(selectedRid)}
+ disabled={embedLoading || !embedTable}
+ className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[.68rem] font-semibold transition-colors"
+ style={{ background: 'color-mix(in srgb, var(--yl) 12%, transparent)', color: 'var(--yl)', border: '1px solid color-mix(in srgb, var(--yl) 25%, transparent)' }}
+ >
+ {embedLoading ? : }
+ {t('rpt.fullEmbedding')}
+
+
+ )}
+
+
+ {/* Embed message */}
+ {embedMsg && (
+
+ {embedMsg.type === 's' ?
:
}
+
{embedMsg.text}
+
setEmbedMsg(null)} className="ml-auto opacity-60 hover:opacity-100">
+
+
+
+ )}
+
+ {/* File sections */}
+
+ {sections.map((sec) => {
+ const secFiles = grouped[sec];
+ const Icon = sectionIcons[sec] || FileText;
+ return (
+
+ {/* Section header */}
+
+
+ {sec}
+ ({secFiles.length})
+
+
+ {/* File grid */}
+
+ {secFiles.map((f) => {
+ const ext = fileExt(f.file_name);
+ const color = extColors[ext] || 'var(--ac)';
+ const canEmbed = ['csv', 'json', 'txt', 'md', 'pdf'].includes(ext);
+ return (
+
{
+ e.currentTarget.style.borderColor = color;
+ e.currentTarget.style.boxShadow = `0 2px 8px ${color}18`;
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.borderColor = 'var(--bd)';
+ e.currentTarget.style.boxShadow = 'none';
+ }}
+ >
+
+
+ {ext}
+
+
+
+
+ {f.file_name}
+
+
+ {formatFileSize(f.file_size)}
+
+
+
+ {canEmbed && adbCfg.length > 0 && (
+
handleEmbedFile(selectedRid, f.id)}
+ disabled={embedLoading}
+ className="p-1.5 rounded-md opacity-30 group-hover:opacity-80 transition-opacity"
+ title={`Embedding: ${f.file_name}`}
+ style={{ color: 'var(--yl)' }}
+ >
+
+
+ )}
+
+
+
+
+
+ );
+ })}
+
+
+ );
+ })}
+
+
+ );
+ })()}
+
+ {/* βββ Report History βββ */}
+
+
+
+
+ {t('rpt.executionHistory')}
+
+
+ {reports.length} {t('rpt.reports')}
+
+
+
+ {reportsLoading && !reports.length ? (
+
+
+ {t('rpt.loading')}
+
+ ) : reports.length === 0 ? (
+
+
+
+
+
{t('rpt.noReports')}.
+
+ {t('rpt.firstScanHint')}
+
+
+ ) : (
+
+ {reports.map((r) => (
+
{
+ if (r.status === 'completed') setSelectedRid(r.id);
+ }}
+ onMouseEnter={(e) => { if (selectedRid !== r.id) e.currentTarget.style.background = 'var(--bg2)'; }}
+ onMouseLeave={(e) => { if (selectedRid !== r.id) e.currentTarget.style.background = ''; }}
+ >
+
+
+
+ {r.tenancy_name}
+
+
+
+ Level {r.level || 2}
+
+ |
+
+ {r.created_at}
+
+ {r.completed_at && (
+ <>
+ |
+
+ {t('rpt.completedAt')} {r.completed_at}
+
+ >
+ )}
+
+
+
+
+ {statusBadge(r.status)}
+ {r.status === 'completed' && selectedRid === r.id && (
+ {t('rpt.selected')}
+ )}
+ {(r.status === 'failed' || r.status === 'cancelled') && (
+ {
+ e.stopPropagation();
+ if (!confirm(t('rpt.deleteConfirm'))) return;
+ try {
+ await reportsApi.delete(r.id);
+ setReports((prev) => prev.filter((x) => x.id !== r.id));
+ } catch (err) {
+ alert(err instanceof Error ? err.message : t('rpt.deleteFailed'));
+ }
+ }}
+ >
+ {t('rpt.delete')}
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+
+
+ );
+}
diff --git a/frontend-react/src/pages/TerraformPage.tsx b/frontend-react/src/pages/TerraformPage.tsx
new file mode 100644
index 0000000..80fd1f1
--- /dev/null
+++ b/frontend-react/src/pages/TerraformPage.tsx
@@ -0,0 +1,1566 @@
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import ReactMarkdown from 'react-markdown';
+import remarkGfm from 'remark-gfm';
+import rehypeHighlight from 'rehype-highlight';
+import {
+ Code, Send, History, ChevronRight, ChevronDown, Trash2, RefreshCw,
+ Play, Rocket, Flame, Ban, Download, Copy, Save, FileCode, Terminal,
+ Settings, Loader2, Plus, Wrench, RotateCcw, X, FolderOpen, Menu,
+ Search, Check, File, Layers,
+} from 'lucide-react';
+import { useAppStore } from '@/stores/app';
+import { useI18n } from '@/i18n';
+import {
+ terraformApi,
+ type Workspace, type WsStatus, type ChatMessage, type ChatSession,
+} from '@/api/endpoints/terraform';
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ Constants & helpers
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+const RUNNING: WsStatus[] = ['planning', 'applying', 'destroying'];
+
+const BADGE: Record = {
+ draft: { cls: 'bg-[var(--bg3)] text-[var(--t3)]', label: 'Draft' },
+ planning: { cls: 'bg-[var(--ppl)] text-[var(--pp)]', label: 'Planning...' },
+ planned: { cls: 'bg-[var(--gnl)] text-[var(--gn)]', label: 'Plan OK' },
+ applying: { cls: 'bg-[var(--bll)] text-[var(--bl)]', label: 'Applying...' },
+ applied: { cls: 'bg-[var(--gnl)] text-[var(--gn)]', label: 'Applied' },
+ destroying: { cls: 'bg-[var(--rdl)] text-[var(--rd)]', label: 'Destroying...' },
+ destroyed: { cls: 'bg-[var(--bg3)] text-[var(--t4)]', label: 'Destroyed' },
+ failed: { cls: 'bg-[var(--rdl)] text-[var(--rd)]', label: 'Failed' },
+};
+
+interface TfFile {
+ name: string;
+ content: string;
+ size: number;
+}
+
+const ALLOWED_MODELS = [
+ { id: 'openai.gpt-4.1', label: 'GPT-4.1', provider: 'OpenAI' },
+ { id: 'openai.o3', label: 'o3', provider: 'OpenAI' },
+ { id: 'openai.o4-mini', label: 'o4-mini', provider: 'OpenAI' },
+ { id: 'openai.gpt-5.1', label: 'GPT-5.1', provider: 'OpenAI' },
+ { id: 'openai.gpt-5.2', label: 'GPT-5.2', provider: 'OpenAI' },
+ { id: 'google.gemini-2.5-pro', label: 'Gemini 2.5 Pro', provider: 'Google' },
+ { id: 'google.gemini-2.5-flash', label: 'Gemini 2.5 Flash', provider: 'Google' },
+];
+
+/* ββ file parsing ββ */
+
+function parseFiles(code: string | undefined | null): TfFile[] {
+ if (!code) return [];
+ const parts = code.split(/^\/\/\s*filename:\s*/m).filter(Boolean);
+ return parts.map((p) => {
+ const nl = p.indexOf('\n');
+ const name = nl > 0 ? p.slice(0, nl).trim() : 'main.tf';
+ const content = nl > 0 ? p.slice(nl + 1) : p;
+ return { name, content, size: new TextEncoder().encode(content).length };
+ });
+}
+
+function joinFiles(files: TfFile[]): string {
+ return files.map((f) => `// filename: ${f.name}\n${f.content}`).join('\n\n');
+}
+
+function extractFilesFromResponse(text: string): TfFile[] | null {
+ // Extract ONLY hcl/terraform tagged code blocks (skip plain/untagged blocks)
+ const rawBlocks: string[] = [];
+ const re = /```(?:hcl|terraform|tf)\s*\n([\s\S]*?)```/g;
+ let m: RegExpExecArray | null;
+ while ((m = re.exec(text)) !== null) rawBlocks.push(m[1]);
+ if (rawBlocks.length === 0) {
+ // Fallback: try untagged blocks that contain // filename:
+ const re2 = /```\s*\n([\s\S]*?)```/g;
+ while ((m = re2.exec(text)) !== null) {
+ if (m[1].includes('// filename:')) rawBlocks.push(m[1]);
+ }
+ if (rawBlocks.length === 0) return null;
+ }
+
+ const allCode = rawBlocks.join('\n\n');
+
+ // If contains // filename: markers, split by them
+ if (allCode.includes('// filename:')) {
+ return parseFiles(allCode);
+ }
+
+ // No filename markers β single file
+ return [{ name: 'main.tf', content: allCode.trim(), size: new TextEncoder().encode(allCode).length }];
+}
+
+/** Merge new files from model response with existing files (keep unchanged files) */
+function mergeFiles(existing: TfFile[], incoming: TfFile[]): TfFile[] {
+ if (!existing.length) return incoming;
+ const newMap: Record = {};
+ for (const f of incoming) newMap[f.name] = f.content;
+ const merged: TfFile[] = [];
+ const used = new Set();
+ // Update existing files or keep them
+ for (const f of existing) {
+ if (newMap[f.name] != null) {
+ merged.push({ name: f.name, content: newMap[f.name], size: new TextEncoder().encode(newMap[f.name]).length });
+ used.add(f.name);
+ } else {
+ merged.push(f);
+ }
+ }
+ // Add new files not in existing
+ for (const [name, content] of Object.entries(newMap)) {
+ if (!used.has(name)) {
+ merged.push({ name, content, size: new TextEncoder().encode(content).length });
+ }
+ }
+ return merged;
+}
+
+/* ββ plan parsing ββ */
+
+interface PlanChange {
+ action: '+' | '~' | '-';
+ type: string;
+ name: string;
+}
+
+function parsePlan(output: string | undefined): PlanChange[] {
+ if (!output) return [];
+ const changes: PlanChange[] = [];
+ const lines = output.split('\n');
+ for (const line of lines) {
+ const trimmed = line.trim();
+ let action: '+' | '~' | '-' | null = null;
+ if (trimmed.startsWith('# ') && trimmed.includes(' will be ')) {
+ const resource = trimmed.slice(2).split(' will be ')[0].trim();
+ const verb = trimmed.split(' will be ')[1]?.toLowerCase() || '';
+ if (verb.includes('created')) action = '+';
+ else if (verb.includes('updated') || verb.includes('changed')) action = '~';
+ else if (verb.includes('destroyed')) action = '-';
+ if (action) {
+ const dot = resource.lastIndexOf('.');
+ changes.push({
+ action,
+ type: dot > 0 ? resource.slice(0, dot) : resource,
+ name: dot > 0 ? resource.slice(dot + 1) : resource,
+ });
+ }
+ }
+ }
+ return changes;
+}
+
+/* ββ terminal coloring ββ */
+
+function colorTerminalLine(line: string): { color: string; bold: boolean } {
+ const t = line.trimStart();
+ if (t.startsWith('+') || t.startsWith('# ') && t.includes('created'))
+ return { color: 'var(--gn)', bold: false };
+ if (t.startsWith('~') || t.startsWith('# ') && t.includes('updated'))
+ return { color: 'var(--yl)', bold: false };
+ if (t.startsWith('-') || t.startsWith('# ') && t.includes('destroyed'))
+ return { color: 'var(--rd)', bold: false };
+ if (t.startsWith('Plan:'))
+ return { color: 'var(--pp)', bold: true };
+ if (t.startsWith('Apply complete'))
+ return { color: 'var(--gn)', bold: true };
+ if (t.startsWith('Error'))
+ return { color: 'var(--rd)', bold: true };
+ return { color: 'var(--t3)', bold: false };
+}
+
+/* ββ date helpers ββ */
+
+function dateLabel(dt: string | undefined): string {
+ const t = useI18n.getState().t;
+ if (!dt) return t('chat.other');
+ const slice = dt.slice(0, 10);
+ const today = new Date().toISOString().slice(0, 10);
+ const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
+ if (slice === today) return t('chat.today');
+ if (slice === yesterday) return t('chat.yesterday');
+ const d = new Date(slice + 'T00:00:00');
+ const diff = Math.floor((Date.now() - d.getTime()) / 86400000);
+ if (diff < 7) return d.toLocaleDateString('pt-BR', { weekday: 'long' });
+ return d.toLocaleDateString('pt-BR', { day: '2-digit', month: 'short', year: 'numeric' });
+}
+
+/* ββ CRC32 + ZIP ββ */
+
+function crc32(buf: Uint8Array): number {
+ let c = 0xFFFFFFFF;
+ const table: number[] = [];
+ for (let n = 0; n < 256; n++) {
+ let v = n;
+ for (let k = 0; k < 8; k++) v = v & 1 ? 0xEDB88320 ^ (v >>> 1) : v >>> 1;
+ table[n] = v;
+ }
+ for (let i = 0; i < buf.length; i++) c = table[(c ^ buf[i]) & 0xFF] ^ (c >>> 8);
+ return (c ^ 0xFFFFFFFF) >>> 0;
+}
+
+function buildZip(files: { name: string; data: Uint8Array }[]): Uint8Array {
+ const enc = new TextEncoder();
+ const entries: { name: Uint8Array; data: Uint8Array; crc: number; offset: number }[] = [];
+ const parts: Uint8Array[] = [];
+ let offset = 0;
+
+ for (const f of files) {
+ const nameBytes = enc.encode(f.name);
+ const crc = crc32(f.data);
+ // local file header: 30 bytes + name
+ const lh = new Uint8Array(30 + nameBytes.length);
+ const lv = new DataView(lh.buffer);
+ lv.setUint32(0, 0x04034b50, true); // sig
+ lv.setUint16(4, 20, true); // version
+ lv.setUint16(6, 0, true); // flags
+ lv.setUint16(8, 0, true); // compression (store)
+ lv.setUint16(10, 0, true); // mod time
+ lv.setUint16(12, 0, true); // mod date
+ lv.setUint32(14, crc, true);
+ lv.setUint32(18, f.data.length, true);
+ lv.setUint32(22, f.data.length, true);
+ lv.setUint16(26, nameBytes.length, true);
+ lv.setUint16(28, 0, true);
+ lh.set(nameBytes, 30);
+
+ entries.push({ name: nameBytes, data: f.data, crc, offset });
+ parts.push(lh, f.data);
+ offset += lh.length + f.data.length;
+ }
+
+ const cdStart = offset;
+ const cdParts: Uint8Array[] = [];
+ for (const e of entries) {
+ const cd = new Uint8Array(46 + e.name.length);
+ const cv = new DataView(cd.buffer);
+ cv.setUint32(0, 0x02014b50, true);
+ cv.setUint16(4, 20, true);
+ cv.setUint16(6, 20, true);
+ cv.setUint16(8, 0, true);
+ cv.setUint16(10, 0, true);
+ cv.setUint16(12, 0, true);
+ cv.setUint16(14, 0, true);
+ cv.setUint32(16, e.crc, true);
+ cv.setUint32(20, e.data.length, true);
+ cv.setUint32(24, e.data.length, true);
+ cv.setUint16(28, e.name.length, true);
+ cv.setUint16(30, 0, true);
+ cv.setUint16(32, 0, true);
+ cv.setUint16(34, 0, true);
+ cv.setUint16(36, 0, true);
+ cv.setUint32(38, 0x20, true);
+ cv.setUint32(42, e.offset, true);
+ cd.set(e.name, 46);
+ cdParts.push(cd);
+ }
+
+ const cdBuf = concatBytes(cdParts);
+ const eocd = new Uint8Array(22);
+ const ev = new DataView(eocd.buffer);
+ ev.setUint32(0, 0x06054b50, true);
+ ev.setUint16(4, 0, true);
+ ev.setUint16(6, 0, true);
+ ev.setUint16(8, entries.length, true);
+ ev.setUint16(10, entries.length, true);
+ ev.setUint32(12, cdBuf.length, true);
+ ev.setUint32(16, cdStart, true);
+ ev.setUint16(20, 0, true);
+
+ return concatBytes([...parts, cdBuf, eocd]);
+}
+
+function concatBytes(arrays: Uint8Array[]): Uint8Array {
+ const total = arrays.reduce((s, a) => s + a.length, 0);
+ const r = new Uint8Array(total);
+ let off = 0;
+ for (const a of arrays) { r.set(a, off); off += a.length; }
+ return r;
+}
+
+function downloadZip(files: TfFile[], wsName: string) {
+ const enc = new TextEncoder();
+ const zipFiles = files.map((f) => ({ name: f.name, data: enc.encode(f.content) }));
+ // auto-generate terraform.tfvars stub
+ const tfvarsContent = `# Auto-generated terraform.tfvars\n# Preencha as variaveis abaixo\n`;
+ if (!files.some((f) => f.name === 'terraform.tfvars')) {
+ zipFiles.push({ name: 'terraform.tfvars', data: enc.encode(tfvarsContent) });
+ }
+ const raw = buildZip(zipFiles);
+ const blob = new Blob([new Uint8Array(raw)], { type: 'application/zip' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `${wsName || 'terraform'}.zip`;
+ a.click();
+ URL.revokeObjectURL(url);
+}
+
+/* ββ compartment flattening ββ */
+
+interface CompartmentNode {
+ id: string;
+ name: string;
+ children?: CompartmentNode[];
+}
+
+interface FlatCompartment {
+ id: string;
+ name: string;
+ depth: number;
+}
+
+function flattenCompartments(nodes: CompartmentNode[], depth = 0): FlatCompartment[] {
+ const result: FlatCompartment[] = [];
+ for (const n of nodes) {
+ result.push({ id: n.id, name: n.name, depth });
+ if (n.children) result.push(...flattenCompartments(n.children, depth + 1));
+ }
+ return result;
+}
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ StatusBadge
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+function StatusBadge({ status }: { status: WsStatus }) {
+ const b = BADGE[status] || BADGE.draft;
+ return (
+
+ {RUNNING.includes(status) && (
+
+ )}
+ {b.label}
+
+ );
+}
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ Modal
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+function Modal({ children, onClose }: { children: React.ReactNode; onClose: () => void }) {
+ return (
+
+
e.stopPropagation()}>
+ {children}
+
+
+ );
+}
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ MAIN COMPONENT
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+export default function TerraformPage() {
+ const { t } = useI18n();
+ const { ociCfg, genaiCfg } = useAppStore();
+
+ /* ββ Chat state ββ */
+ const [messages, setMessages] = useState([]);
+ const [sessionId, setSessionId] = useState(null);
+ const [historyOpen, setHistoryOpen] = useState(false);
+ const [sessions, setSessions] = useState([]);
+ const [chatInput, setChatInput] = useState('');
+ const [sending, setSending] = useState(false);
+ const chatEndRef = useRef(null);
+ const inputRef = useRef(null);
+
+ /* ββ Model state ββ */
+ const [model, setModel] = useState('openai.gpt-4.1');
+ const [ociConfig, setOciConfig] = useState('');
+ const [region, setRegion] = useState('');
+ const [compartment, setCompartment] = useState('');
+ const [compartments, setCompartments] = useState([]);
+ const [modelDropdownOpen, setModelDropdownOpen] = useState(false);
+ const [modelSearch, setModelSearch] = useState('');
+ const [menuOpen, setMenuOpen] = useState(false);
+ const [tfTemperature, setTfTemperature] = useState(0.3);
+
+ /* ββ Workspace state ββ */
+ const [wsId, setWsId] = useState(null);
+ const [wsStatus, setWsStatus] = useState('draft');
+ const [wsName, setWsName] = useState('');
+ const [files, setFiles] = useState([]);
+ const [planOutput, setPlanOutput] = useState('');
+ const [applyOutput, setApplyOutput] = useState('');
+ const [destroyOutput, setDestroyOutput] = useState('');
+ const [wsError, setWsError] = useState('');
+
+ /* ββ UI state ββ */
+ const [editingFileIdx, setEditingFileIdx] = useState(null);
+ const [bottomTab, setBottomTab] = useState<'files' | 'plan' | 'resources'>('files');
+ const [running, setRunning] = useState(false);
+ const [terminalView, setTerminalView] = useState<'plan' | 'apply' | 'destroy'>('plan');
+ const terminalRef = useRef(null);
+
+ /* ββ Resources ββ */
+ const [resources, setResources] = useState>({});
+ const [resourcesLoading, setResourcesLoading] = useState(false);
+
+ /* ββ Confirm modals ββ */
+ const [confirmDestroy, setConfirmDestroy] = useState(false);
+ const [destroyInput, setDestroyInput] = useState('');
+
+ /* ββ Bottom panel resize ββ */
+ const [bottomHeight, setBottomHeight] = useState(280);
+ const resizingRef = useRef(false);
+ const containerRef = useRef(null);
+
+ /* poll ref to cancel */
+ const pollCancelRef = useRef(false);
+
+ /* ββ Init: OCI config default ββ */
+ useEffect(() => {
+ if (!ociConfig && ociCfg.length > 0) {
+ setOciConfig(ociCfg[0].id);
+ setRegion(ociCfg[0].region || '');
+ }
+ }, [ociCfg, ociConfig]);
+
+ /* ββ Load compartments when OCI config changes ββ */
+ useEffect(() => {
+ if (!ociConfig) return;
+ const cfg = ociCfg.find((c) => c.id === ociConfig);
+ if (cfg) {
+ setRegion(cfg.region || '');
+ if (cfg.compartment_id) {
+ terraformApi.getCompartmentTree(ociConfig).then((tree) => {
+ setCompartments(flattenCompartments(tree));
+ if (!compartment && cfg.compartment_id) setCompartment(cfg.compartment_id);
+ }).catch(() => {
+ setCompartments([]);
+ });
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [ociConfig, ociCfg]);
+
+ /* ββ URL param: ?ws=WID ββ */
+ useEffect(() => {
+ const params = new URLSearchParams(window.location.search);
+ const wsParam = params.get('ws');
+ if (wsParam) loadWorkspace(wsParam);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ /* ββ Auto-scroll chat ββ */
+ useEffect(() => {
+ chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ }, [messages]);
+
+ /* ββ Auto-scroll terminal ββ */
+ useEffect(() => {
+ if (terminalRef.current) {
+ terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
+ }
+ }, [planOutput, applyOutput, destroyOutput]);
+
+ /* ββ Load sessions for history ββ */
+ const loadSessions = useCallback(async () => {
+ try {
+ const s = await terraformApi.listSessions('terraform', 50);
+ setSessions(s);
+ } catch { setSessions([]); }
+ }, []);
+
+ useEffect(() => { loadSessions(); }, [loadSessions]);
+
+ /* ββ Load workspace ββ */
+ const loadWorkspace = useCallback(async (wid: string) => {
+ try {
+ const ws = await terraformApi.getWorkspace(wid);
+ setWsId(ws.id);
+ setWsName(ws.name);
+ setWsStatus(ws.status);
+ setPlanOutput(ws.plan_output || '');
+ setApplyOutput(ws.apply_output || '');
+ setDestroyOutput(ws.destroy_output || '');
+ setWsError(ws.error || '');
+ setFiles(parseFiles(ws.tf_code));
+ setSessionId(ws.session_id);
+ if (ws.oci_config_id) setOciConfig(ws.oci_config_id);
+ if (ws.compartment_id) setCompartment(ws.compartment_id);
+
+ // Load chat messages
+ if (ws.session_id) {
+ try {
+ const r = await terraformApi.loadSession(ws.session_id);
+ setMessages(r.messages || []);
+ } catch { /* no messages */ }
+ }
+
+ // Auto-select terminal view
+ if (ws.destroy_output) setTerminalView('destroy');
+ else if (ws.apply_output) setTerminalView('apply');
+ else if (ws.plan_output) setTerminalView('plan');
+
+ // If running, start polling
+ if (RUNNING.includes(ws.status)) {
+ setRunning(true);
+ pollWorkspaceStatus(ws.id);
+ }
+ } catch (e: unknown) {
+ alert('Erro ao carregar workspace: ' + (e as Error).message);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ /* ββ Load session (chat history + associated workspace) ββ */
+ const loadSession = useCallback(async (sid: string) => {
+ try {
+ const r = await terraformApi.loadSession(sid);
+ setMessages(r.messages || []);
+ setSessionId(sid);
+ setHistoryOpen(false);
+
+ // Try to find and load the workspace associated with this session
+ try {
+ const workspaces = await terraformApi.listWorkspaces();
+ const ws = workspaces.find((w) => w.session_id === sid);
+ if (ws) {
+ await loadWorkspace(ws.id);
+ } else {
+ // No workspace β reset workspace state but keep messages
+ setWsId(null);
+ setWsName('');
+ setWsStatus('draft');
+ setFiles([]);
+ setPlanOutput('');
+ setApplyOutput('');
+ setDestroyOutput('');
+ setWsError('');
+ }
+ } catch { /* no workspace */ }
+ } catch (e: unknown) {
+ alert('Erro: ' + (e as Error).message);
+ }
+ }, [loadWorkspace]);
+
+ /* ββ Delete session ββ */
+ const deleteSession = useCallback(async (sid: string) => {
+ try {
+ await terraformApi.deleteSession(sid);
+ setSessions((prev) => prev.filter((s) => s.id !== sid));
+ if (sessionId === sid) {
+ setSessionId(null);
+ setMessages([]);
+ }
+ } catch { /* noop */ }
+ }, [sessionId]);
+
+ /* ββ New chat ββ */
+ const newChat = useCallback(() => {
+ setSessionId(null);
+ setMessages([]);
+ setWsId(null);
+ setWsName('');
+ setWsStatus('draft');
+ setFiles([]);
+ setPlanOutput('');
+ setApplyOutput('');
+ setDestroyOutput('');
+ setWsError('');
+ setEditingFileIdx(null);
+ }, []);
+
+ /* ββ Model resolution ββ */
+ const resolveModel = useCallback(() => {
+ if (model.startsWith('cfg:')) {
+ const cfgId = model.slice(4);
+ return { genai_config_id: cfgId };
+ }
+ return { model_id: model, oci_config_id: ociConfig, genai_region: region };
+ }, [model, ociConfig, region]);
+
+ /* ββ Send chat message ββ */
+ const sendMessage = useCallback(async (overrideMsg?: string) => {
+ const text = (overrideMsg || chatInput).trim();
+ if (!text || sending) return;
+ setSending(true);
+ setChatInput('');
+
+ // Build context (aligned with legacy frontend)
+ let fullMessage = text;
+ if (files.length > 0) {
+ const filesCtx = files.map((f) => `// filename: ${f.name}\n${f.content}`).join('\n\n');
+ fullMessage += '\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 (planOutput) {
+ fullMessage += '\n\n---\nΓLTIMO OUTPUT DO TERRAFORM PLAN:\n```\n' + planOutput.slice(-3000) + '\n```';
+ }
+ if (applyOutput) {
+ fullMessage += '\n\n---\nΓLTIMO OUTPUT DO TERRAFORM APPLY:\n```\n' + applyOutput.slice(-3000) + '\n```';
+ }
+
+ // Add user message to UI immediately
+ const userMsg: ChatMessage = {
+ id: 'tmp-' + Date.now(),
+ role: 'user',
+ content: text,
+ created_at: new Date().toISOString(),
+ };
+ setMessages((prev) => [...prev, userMsg]);
+
+ try {
+ const modelCfg = resolveModel();
+ const res = await terraformApi.chat({
+ message: fullMessage,
+ session_id: sessionId || undefined,
+ ...modelCfg,
+ compartment_id: compartment || undefined,
+ temperature: tfTemperature,
+ use_tools: true,
+ });
+
+ if (!sessionId) setSessionId(res.session_id);
+
+ // Poll for response
+ const mid = res.message_id;
+ const assistantMsg: ChatMessage = {
+ id: mid,
+ role: 'assistant',
+ content: '',
+ created_at: new Date().toISOString(),
+ };
+ setMessages((prev) => [...prev, assistantMsg]);
+
+ for (let i = 0; i < 600; i++) {
+ await new Promise((r) => setTimeout(r, 2000));
+ try {
+ const st = await terraformApi.pollMessageStatus(mid);
+ setMessages((prev) =>
+ prev.map((m) => m.id === mid ? { ...m, content: st.content || '' } : m)
+ );
+ if (st.status === 'done' || st.status === 'failed') {
+ // Extract files from response and merge with existing
+ if (st.status === 'done' && st.content) {
+ const extracted = extractFilesFromResponse(st.content);
+ if (extracted && extracted.length > 0) {
+ setFiles((prev) => prev.length > 0 ? mergeFiles(prev, extracted) : extracted);
+ setBottomTab('files');
+ }
+ }
+ break;
+ }
+ } catch { /* keep polling */ }
+ }
+
+ loadSessions();
+ } catch (e: unknown) {
+ setMessages((prev) => [
+ ...prev,
+ { id: 'err-' + Date.now(), role: 'assistant', content: `Erro: ${(e as Error).message}`, created_at: new Date().toISOString() },
+ ]);
+ }
+ setSending(false);
+ }, [chatInput, sending, sessionId, files, planOutput, applyOutput, wsError, resolveModel, compartment, loadSessions]);
+
+ /* ββ Poll workspace status ββ */
+ const pollWorkspaceStatus = useCallback(async (wid: string) => {
+ pollCancelRef.current = false;
+ setRunning(true);
+ for (let i = 0; i < 1800; i++) {
+ if (pollCancelRef.current) break;
+ await new Promise((r) => setTimeout(r, 2000));
+ try {
+ const r = await terraformApi.status(wid);
+ setWsStatus(r.status);
+ if (r.plan_output) setPlanOutput(r.plan_output);
+ if (r.apply_output) setApplyOutput(r.apply_output);
+ if (r.destroy_output) setDestroyOutput(r.destroy_output);
+ if (r.error) setWsError(r.error);
+ if (!RUNNING.includes(r.status)) {
+ setRunning(false);
+ return;
+ }
+ } catch { /* keep polling */ }
+ }
+ setRunning(false);
+ }, []);
+
+ /* ββ Save & Plan ββ */
+ const doPlan = useCallback(async () => {
+ if (files.length === 0) { alert('Nenhum arquivo para planejar'); return; }
+ const code = joinFiles(files);
+ try {
+ let wid = wsId;
+ if (!wid) {
+ const ws = await terraformApi.createWorkspace({
+ session_id: sessionId || undefined,
+ oci_config_id: ociConfig,
+ compartment_id: compartment || undefined,
+ name: wsName || files[0]?.name || 'workspace',
+ tf_code: code,
+ });
+ wid = ws.id;
+ setWsId(ws.id);
+ setWsName(ws.name);
+ setWsStatus(ws.status);
+ } else {
+ await terraformApi.updateCode(wid, code);
+ }
+ await terraformApi.plan(wid);
+ setWsStatus('planning');
+ setTerminalView('plan');
+ setPlanOutput('');
+ setWsError('');
+ pollWorkspaceStatus(wid);
+ } catch (e: unknown) {
+ alert('Erro: ' + (e as Error).message);
+ }
+ }, [files, wsId, sessionId, ociConfig, compartment, wsName, pollWorkspaceStatus]);
+
+ /* ββ Apply ββ */
+ const doApply = useCallback(async () => {
+ if (!wsId) return;
+ try {
+ await terraformApi.apply(wsId);
+ setWsStatus('applying');
+ setTerminalView('apply');
+ setApplyOutput('');
+ pollWorkspaceStatus(wsId);
+ } catch (e: unknown) {
+ alert('Erro: ' + (e as Error).message);
+ }
+ }, [wsId, pollWorkspaceStatus]);
+
+ /* ββ Destroy ββ */
+ const doDestroy = useCallback(async () => {
+ if (!wsId || destroyInput !== 'DESTROY') return;
+ setConfirmDestroy(false);
+ setDestroyInput('');
+ try {
+ await terraformApi.destroy(wsId);
+ setWsStatus('destroying');
+ setTerminalView('destroy');
+ setDestroyOutput('');
+ pollWorkspaceStatus(wsId);
+ } catch (e: unknown) {
+ alert('Erro: ' + (e as Error).message);
+ }
+ }, [wsId, destroyInput, pollWorkspaceStatus]);
+
+ /* ββ Cancel ββ */
+ const doCancel = useCallback(async () => {
+ if (!wsId) return;
+ pollCancelRef.current = true;
+ try { await terraformApi.cancel(wsId); } catch { /* noop */ }
+ setRunning(false);
+ if (wsId) {
+ try {
+ const r = await terraformApi.status(wsId);
+ setWsStatus(r.status);
+ } catch { /* noop */ }
+ }
+ }, [wsId]);
+
+ /* ββ Auto-fix ββ */
+ const doAutoFix = useCallback(() => {
+ if (!wsError && !planOutput) return;
+ const errLines = planOutput.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') : (wsError || planOutput.slice(-2000));
+ const fixPrompt = `O terraform plan falhou com os seguintes erros. Corrija SOMENTE os arquivos afetados.\n\nERROS DO PLAN:\n\`\`\`\n${errCtx}\n\`\`\``;
+ sendMessage(fixPrompt);
+ }, [wsError, planOutput, sendMessage]);
+
+ /* ββ Rollback (re-plan with previous code) ββ */
+ const doRollback = useCallback(() => {
+ if (!wsId) return;
+ // Reload workspace to get original code
+ loadWorkspace(wsId);
+ }, [wsId, loadWorkspace]);
+
+ /* ββ Load resources ββ */
+ const loadResources = useCallback(async () => {
+ if (!ociConfig || !compartment) {
+ alert('Selecione OCI Config e Compartment');
+ return;
+ }
+ setResourcesLoading(true);
+ try {
+ const r = await terraformApi.loadResources(ociConfig, compartment, region || undefined);
+ setResources(r);
+ } catch (e: unknown) {
+ alert('Erro: ' + (e as Error).message);
+ setResources({});
+ }
+ setResourcesLoading(false);
+ }, [ociConfig, compartment, region]);
+
+ /* ββ File editing ββ */
+ const updateFileContent = useCallback((idx: number, content: string) => {
+ setFiles((prev) => prev.map((f, i) =>
+ i === idx ? { ...f, content, size: new TextEncoder().encode(content).length } : f
+ ));
+ }, []);
+
+ const copyFile = useCallback((content: string) => {
+ navigator.clipboard.writeText(content);
+ }, []);
+
+ const downloadFile = useCallback((file: TfFile) => {
+ const blob = new Blob([file.content], { type: 'text/plain' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = file.name;
+ a.click();
+ URL.revokeObjectURL(url);
+ }, []);
+
+ /* ββ Bottom panel resize ββ */
+ const onResizeStart = useCallback((e: React.MouseEvent) => {
+ e.preventDefault();
+ resizingRef.current = true;
+ const startY = e.clientY;
+ const startH = bottomHeight;
+ const onMove = (ev: MouseEvent) => {
+ if (!resizingRef.current) return;
+ const diff = startY - ev.clientY;
+ const newH = Math.max(150, Math.min(startH + diff, window.innerHeight * 0.6));
+ setBottomHeight(newH);
+ };
+ const onUp = () => {
+ resizingRef.current = false;
+ window.removeEventListener('mousemove', onMove);
+ window.removeEventListener('mouseup', onUp);
+ };
+ window.addEventListener('mousemove', onMove);
+ window.addEventListener('mouseup', onUp);
+ }, [bottomHeight]);
+
+ /* ββ Session groups for history sidebar ββ */
+ const sessionGroups = useMemo(() => {
+ const groups: { label: string; items: ChatSession[] }[] = [];
+ const gMap: Record = {};
+ for (const s of sessions) {
+ const dt = (s.updated_at || s.created_at || '').slice(0, 10);
+ const label = dateLabel(dt);
+ if (gMap[label] === undefined) { gMap[label] = groups.length; groups.push({ label, items: [] }); }
+ groups[gMap[label]].items.push(s);
+ }
+ return groups;
+ }, [sessions]);
+
+ /* ββ Model options (allowed + GenAI configs) ββ */
+ const modelOptions = useMemo(() => {
+ const opts: { id: string; label: string; group: string }[] = [];
+ for (const m of ALLOWED_MODELS) {
+ opts.push({ id: m.id, label: m.label, group: m.provider });
+ }
+ for (const c of genaiCfg) {
+ opts.push({ id: `cfg:${c.id}`, label: c.name, group: 'OCI GenAI' });
+ }
+ return opts;
+ }, [genaiCfg]);
+
+ const filteredModels = useMemo(() => {
+ if (!modelSearch) return modelOptions;
+ const q = modelSearch.toLowerCase();
+ return modelOptions.filter((m) =>
+ m.label.toLowerCase().includes(q) || m.group.toLowerCase().includes(q)
+ );
+ }, [modelOptions, modelSearch]);
+
+ const modelGroups = useMemo(() => {
+ const groups: Record = {};
+ for (const m of filteredModels) {
+ if (!groups[m.group]) groups[m.group] = [];
+ groups[m.group].push({ id: m.id, label: m.label });
+ }
+ return groups;
+ }, [filteredModels]);
+
+ const selectedModelLabel = useMemo(() => {
+ const opt = modelOptions.find((m) => m.id === model);
+ return opt?.label || model;
+ }, [model, modelOptions]);
+
+ /* ββ Plan changes ββ */
+ const planChanges = useMemo(() => parsePlan(planOutput), [planOutput]);
+
+ /* ββ Current terminal output ββ */
+ const terminalOutput = useMemo(() => {
+ if (terminalView === 'apply') return applyOutput;
+ if (terminalView === 'destroy') return destroyOutput;
+ return planOutput;
+ }, [terminalView, planOutput, applyOutput, destroyOutput]);
+
+ const terminalHeaderColor = useMemo(() => {
+ if (terminalView === 'apply') return 'var(--gn)';
+ if (terminalView === 'destroy') return 'var(--rd)';
+ return 'var(--pp)';
+ }, [terminalView]);
+
+ /* ββ Resource categories ββ */
+ const RESOURCE_CATEGORIES: Record = {
+ vcns: 'VCNs',
+ subnets: 'Subnets',
+ instances: 'Instances',
+ gateways: 'Gateways',
+ route_tables: 'Route Tables',
+ security_lists: 'Security Lists',
+ databases: 'Databases',
+ block_volumes: 'Block Volumes',
+ load_balancers: 'Load Balancers',
+ };
+
+ /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ RENDER
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+ return (
+
+
+ {/* βββββββββββββββ LEFT PANEL β CHAT βββββββββββββββ */}
+
+
+ {/* ββ History sidebar (collapsible) ββ */}
+ {historyOpen && (
+
+
+
+
{t('tf.history')}
+
+
+
+
+
setHistoryOpen(false)}
+ className="p-1 rounded cursor-pointer" style={{ color: 'var(--t4)' }}>
+
+
+
+
+ {sessionGroups.map((g) => (
+
+
{g.label}
+ {g.items.map((s) => (
+
loadSession(s.id)}>
+
+ {s.title || t('chat.untitled')}
+
+ { e.stopPropagation(); deleteSession(s.id); }}
+ title={t('common.delete')}>
+
+
+
+ ))}
+
+ ))}
+ {sessions.length === 0 && (
+
+ {t('tf.noConversations')}
+
+ )}
+
+
+ )}
+
+ {/* ββ Chat toolbar ββ */}
+
+
{ setHistoryOpen(!historyOpen); if (!historyOpen) loadSessions(); }}
+ className="p-1.5 rounded-lg cursor-pointer transition-colors"
+ style={{ background: 'var(--bg3)', color: 'var(--t2)' }}
+ title={t('tf.history')}>
+
+
+
+ {/* Model dropdown */}
+
+
setModelDropdownOpen(!modelDropdownOpen)}
+ className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-[.68rem] font-medium cursor-pointer transition-colors"
+ style={{ background: 'var(--bg3)', color: 'var(--t2)', border: '1px solid var(--bd)' }}>
+
+ {selectedModelLabel}
+
+
+ {modelDropdownOpen && (
+
+
+
+
+ setModelSearch(e.target.value)}
+ placeholder={t('tf.searchModel')}
+ className="flex-1 bg-transparent text-[.7rem] outline-none"
+ style={{ color: 'var(--t1)' }}
+ autoFocus />
+
+
+
+ {Object.entries(modelGroups).map(([group, items]) => (
+
+
{group}
+ {items.map((m) => (
+
{ setModel(m.id); setModelDropdownOpen(false); setModelSearch(''); }}
+ className="flex items-center gap-2 w-full px-3 py-1.5 text-left cursor-pointer transition-colors"
+ style={{
+ background: model === m.id ? 'var(--bg3)' : 'transparent',
+ color: 'var(--t2)',
+ }}>
+ {model === m.id && }
+ {m.label}
+
+ ))}
+
+ ))}
+
+
+ )}
+
+
+ {/* OCI Config */}
+
setOciConfig(e.target.value)}
+ className="px-2 py-1.5 rounded-lg text-[.68rem] cursor-pointer"
+ style={{ background: 'var(--bg3)', color: 'var(--t2)', border: '1px solid var(--bd)' }}>
+ {t('tf.ociConfig')}
+ {ociCfg.map((c) => (
+ {c.tenancy_name || c.region}
+ ))}
+
+
+ {/* Compartment */}
+
setCompartment(e.target.value)}
+ className="px-2 py-1.5 rounded-lg text-[.68rem] cursor-pointer max-w-[140px]"
+ style={{ background: 'var(--bg3)', color: 'var(--t2)', border: '1px solid var(--bd)' }}>
+ {t('tf.compartment')}
+ {compartments.map((c) => (
+
+ {' '.repeat(c.depth)}{c.name}
+
+ ))}
+
+
+ {/* Region */}
+ {region && (
+
+ {region}
+
+ )}
+
+
+
+ {/* Menu button */}
+
+
setMenuOpen(!menuOpen)}
+ className="p-1.5 rounded-lg cursor-pointer transition-colors"
+ style={{ background: 'var(--bg3)', color: 'var(--t3)' }} title={t('tf.settings')}>
+
+
+ {menuOpen && (
+
+ {/* Temperature slider */}
+
e.stopPropagation()}>
+
+ Temperature
+ {tfTemperature.toFixed(1)}
+
+
setTfTemperature(+e.target.value)}
+ className="w-full" style={{ accentColor: 'var(--pp)' }} />
+
+
+
{ setMenuOpen(false); /* TODO: system prompt editor */ }}
+ className="flex items-center gap-2 w-full px-3 py-2 text-left cursor-pointer transition-colors"
+ style={{ color: 'var(--t2)' }}>
+
+ Editar System Prompt
+
+
{ setMenuOpen(false); /* TODO: refresh reference */ }}
+ className="flex items-center gap-2 w-full px-3 py-2 text-left cursor-pointer transition-colors"
+ style={{ color: 'var(--t2)' }}>
+
+ Refresh Reference
+
+
+ )}
+
+
+
+ {/* ββ Messages area ββ */}
+
+ {messages.length === 0 && (
+
+
+
{t('tf.emptyTitle')}
+
+ {t('tf.emptySubtitle')}
+
+
+ )}
+ {messages.map((msg) => (
+
+
+ {msg.role === 'assistant' && !msg.content && (
+
+ {t('tf.thinking')}
+
+ )}
+ {msg.role === 'assistant' && msg.content ? (
+
+
+ {msg.content}
+
+
+ ) : msg.role === 'user' ? (
+
{msg.content}
+ ) : null}
+
+
+ ))}
+
+
+
+ {/* ββ Input area ββ */}
+
+
+ {/* end left panel (chat only) */}
+
+ {/* βββββββββββββββ RIGHT PANEL β TERMINAL + BOTTOM PANEL βββββββββββββββ */}
+
+ {/* Terminal header */}
+
+
+
{t('tf.terminal')}
+
+
+ {/* Terminal view toggle */}
+ {(['plan', 'apply', 'destroy'] as const).map((view) => (
+
setTerminalView(view)}
+ className="px-2 py-0.5 rounded text-[.62rem] font-medium cursor-pointer transition-colors"
+ style={{
+ background: terminalView === view
+ ? (view === 'plan' ? 'var(--pp)' : view === 'apply' ? 'var(--gn)' : 'var(--rd)')
+ : 'var(--bg3)',
+ color: terminalView === view ? '#fff' : 'var(--t4)',
+ }}>
+ {view === 'plan' ? t('tf.plan') : view === 'apply' ? t('tf.apply') : t('tf.destroy')}
+
+ ))}
+
+ {running && (
+
+ Executando...
+
+ )}
+
+
+ {/* Terminal body */}
+
+ {!terminalOutput && !running && (
+
+ {`# Terminal de saida do Terraform\n# Execute Plan, Apply ou Destroy para ver os resultados aqui`}
+
+ )}
+ {wsError && terminalView === 'plan' && (
+
+ {'Error: ' + wsError + '\n\n'}
+
+ )}
+ {terminalOutput && terminalOutput.split('\n').map((line, i) => {
+ const { color, bold } = colorTerminalLine(line);
+ return (
+
+ {line + '\n'}
+
+ );
+ })}
+
+
+ {/* βββββββββββββββ BOTTOM PANEL (FILES / PLAN / RESOURCES) βββββββββββββββ */}
+
+ {/* Resize handle */}
+
+
+ {/* Bottom panel content */}
+
+ {/* Tab bar */}
+
+ {([
+ { key: 'files' as const, icon:
, label: t('tf.files') },
+ { key: 'plan' as const, icon:
, label: t('tf.plan') },
+ { key: 'resources' as const, icon:
, label: t('tf.resources') },
+ ]).map((tab) => (
+
setBottomTab(tab.key)}
+ className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[.68rem] font-medium cursor-pointer transition-colors"
+ style={{
+ background: bottomTab === tab.key ? 'var(--pp)' : 'transparent',
+ color: bottomTab === tab.key ? '#fff' : 'var(--t3)',
+ }}>
+ {tab.icon}
+ {tab.label}
+ {tab.key === 'files' && files.length > 0 && (
+
+ {files.length}
+
+ )}
+
+ ))}
+
+
+
+ {/* Status badge */}
+ {wsId &&
}
+
+ {/* Action buttons */}
+
+ {(wsStatus === 'draft' || wsStatus === 'destroyed' || wsStatus === 'failed' || wsStatus === 'planned' || wsStatus === 'applied') && !running && (
+
+ {wsStatus === 'planned' || wsStatus === 'applied' || wsStatus === 'failed' ? (
+ <> Re-plan>
+ ) : (
+ <> {t('tf.plan')}>
+ )}
+
+ )}
+ {wsStatus === 'planned' && !running && (
+
+ {t('tf.apply')}
+
+ )}
+ {wsStatus === 'failed' && !running && (
+
+ Auto-corrigir
+
+ )}
+ {wsStatus === 'applied' && !running && (
+
{ setConfirmDestroy(true); setDestroyInput(''); }}
+ className="flex items-center gap-1 px-2 py-1 rounded-md text-[.66rem] font-medium cursor-pointer transition-colors"
+ style={{ background: 'var(--rd)', color: '#fff', border: '1px solid var(--rd)' }}>
+ {t('tf.destroy')}
+
+ )}
+ {wsStatus === 'failed' && wsId && applyOutput && !running && (
+
+ {t('tf.rollback')}
+
+ )}
+ {running && (
+
+ {t('tf.cancel')}
+
+ )}
+ {files.length > 0 && (
+
downloadZip(files, wsName || 'terraform')}
+ className="flex items-center gap-1 px-2 py-1 rounded-md text-[.66rem] font-medium cursor-pointer transition-colors"
+ style={{ background: 'var(--bg3)', color: 'var(--t2)', border: '1px solid var(--bd)' }}>
+ ZIP
+
+ )}
+
+
+
+ {/* Tab content */}
+
+ {/* ββ FILES TAB ββ */}
+ {bottomTab === 'files' && (
+
+ {files.length === 0 && (
+
+
+
{t('tf.noFiles')}
+
{t('tf.emptySubtitle')}
+
+ )}
+ {files.map((file, idx) => (
+
+
setEditingFileIdx(editingFileIdx === idx ? null : idx)}>
+
+
{file.name}
+
+ {file.size > 1024 ? (file.size / 1024).toFixed(1) + ' KB' : file.size + ' B'}
+
+
+
e.stopPropagation()}>
+ copyFile(file.content)}
+ className="p-1 rounded cursor-pointer" style={{ color: 'var(--t4)' }} title={t('tf.copy')}>
+
+
+ downloadFile(file)}
+ className="p-1 rounded cursor-pointer" style={{ color: 'var(--t4)' }} title={t('common.download')}>
+
+
+
+
+
+ {editingFileIdx === idx && (
+
+ )}
+
+ ))}
+
+ )}
+
+ {/* ββ PLAN TAB ββ */}
+ {bottomTab === 'plan' && (
+
+ {planChanges.length === 0 && !planOutput && (
+
+
+
Nenhum plan executado
+
Execute Plan para ver os recursos
+
+ )}
+ {planChanges.length > 0 && (
+
+
+ Recursos ({planChanges.length})
+
+ {planChanges.map((ch, i) => (
+
+
+ {ch.action}
+
+ {ch.type}
+ .{ch.name}
+
+ ))}
+
+ )}
+ {planChanges.length === 0 && planOutput && (
+
+
Plan Output
+
+ {planOutput.slice(0, 2000)}
+
+
+ )}
+
+ )}
+
+ {/* ββ RESOURCES TAB ββ */}
+ {bottomTab === 'resources' && (
+
+
+
+ {resourcesLoading ? : }
+ {resourcesLoading ? 'Carregando...' : 'Carregar Recursos'}
+
+ {ociConfig && compartment && (
+
+ {ociCfg.find((c) => c.id === ociConfig)?.tenancy_name} / {region}
+
+ )}
+
+
+ {Object.keys(resources).length === 0 && !resourcesLoading && (
+
+
+
Nenhum recurso carregado
+
Selecione OCI Config + Compartment e clique em Carregar
+
+ )}
+
+ {Object.entries(resources).length > 0 && (
+
+ {Object.entries(resources).map(([cat, items]) => {
+ const label = RESOURCE_CATEGORIES[cat] || cat;
+ const display = items.slice(0, 10);
+ const overflow = items.length - 10;
+ return (
+
+
+ {label}
+
+ {items.length}
+
+
+ {display.map((item: any, i: number) => (
+
+ {item.display_name || item.name || item.id || JSON.stringify(item).slice(0, 60)}
+
+ ))}
+ {overflow > 0 && (
+
+ +{overflow} mais
+
+ )}
+
+ );
+ })}
+
+ )}
+
+ )}
+
+
+
+
+ {/* βββββββββββββββ MODALS βββββββββββββββ */}
+
+ {/* Destroy confirm */}
+ {confirmDestroy && (
+
setConfirmDestroy(false)}>
+ {t('ws.confirmDestroy')}
+
+ {t('ws.confirmDestroyMsg')}
+
+ setDestroyInput(e.target.value)}
+ placeholder="DESTROY"
+ className="w-full px-3 py-2 rounded-lg text-sm text-center"
+ style={{
+ background: 'var(--bg2)', color: 'var(--t1)',
+ border: `1px solid ${destroyInput === 'DESTROY' ? 'var(--gn)' : 'var(--rd)'}`,
+ }}
+ autoFocus
+ onKeyDown={(e) => { if (e.key === 'Enter') doDestroy(); }}
+ />
+
+ setConfirmDestroy(false)}
+ className="px-3 py-1.5 rounded-lg text-[.76rem] font-medium cursor-pointer"
+ style={{ background: 'var(--bg3)', color: 'var(--t2)', border: '1px solid var(--bd)' }}>
+ {t('tf.cancel')}
+
+
+ {t('tf.destroy')}
+
+
+
+ )}
+
+ {/* Close model dropdown on click outside */}
+ {modelDropdownOpen && (
+
{ setModelDropdownOpen(false); setModelSearch(''); }} />
+ )}
+ {menuOpen && (
+
setMenuOpen(false)} />
+ )}
+
+ );
+}
diff --git a/frontend-react/src/pages/WorkspacesPage.tsx b/frontend-react/src/pages/WorkspacesPage.tsx
new file mode 100644
index 0000000..732e889
--- /dev/null
+++ b/frontend-react/src/pages/WorkspacesPage.tsx
@@ -0,0 +1,462 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import {
+ FolderOpen, RefreshCw, ChevronRight, Cloud, Download,
+ Play, Rocket, Flame, X, ExternalLink, Ban, Clock,
+} from 'lucide-react';
+import { useAppStore } from '@/stores/app';
+import { useI18n } from '@/i18n';
+import { terraformApi, type Workspace, type WsStatus } from '@/api/endpoints/terraform';
+
+/* ββ helpers ββ */
+
+type FilterKey = 'all' | 'active' | 'draft' | 'failed' | 'destroyed';
+
+const RUNNING: WsStatus[] = ['planning', 'applying', 'destroying'];
+const ACTIVE: WsStatus[] = ['applied', 'planned', 'planning', 'applying'];
+
+function dateLabel(dt: string | undefined): string {
+ const tFn = useI18n.getState().t;
+ if (!dt) return tFn('ws.noDate');
+ const slice = dt.slice(0, 10);
+ const today = new Date().toISOString().slice(0, 10);
+ const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
+ if (slice === today) return tFn('chat.today');
+ if (slice === yesterday) return tFn('chat.yesterday');
+ const d = new Date(slice + 'T00:00:00');
+ const diff = Math.floor((Date.now() - d.getTime()) / 86400000);
+ if (diff < 7) return d.toLocaleDateString('pt-BR', { weekday: 'long' });
+ return d.toLocaleDateString('pt-BR', { day: '2-digit', month: 'short', year: 'numeric' });
+}
+
+function timeAgo(ts: string | undefined): string {
+ if (!ts) return 'β';
+ const d = new Date(ts + 'Z');
+ const s = Math.floor((Date.now() - d.getTime()) / 1000);
+ const tFn = useI18n.getState().t;
+ if (s < 60) return tFn('chat.timeNow');
+ if (s < 3600) return Math.floor(s / 60) + 'min';
+ if (s < 86400) return Math.floor(s / 3600) + 'h';
+ return Math.floor(s / 86400) + 'd';
+}
+
+const BADGE: Record
= {
+ draft: { cls: 'bg-[var(--bg3)] text-[var(--t3)]', label: 'Draft' },
+ planning: { cls: 'bg-[var(--ppl)] text-[var(--pp)]', label: 'Planning...' },
+ planned: { cls: 'bg-[var(--gnl)] text-[var(--gn)]', label: 'Plan OK' },
+ applying: { cls: 'bg-[var(--bll)] text-[var(--bl)]', label: 'Applying...' },
+ applied: { cls: 'bg-[var(--gnl)] text-[var(--gn)]', label: 'Applied' },
+ destroying: { cls: 'bg-[var(--rdl)] text-[var(--rd)]', label: 'Destroying...' },
+ destroyed: { cls: 'bg-[var(--bg3)] text-[var(--t4)]', label: 'Destroyed' },
+ failed: { cls: 'bg-[var(--rdl)] text-[var(--rd)]', label: 'Failed' },
+};
+
+function StatusBadge({ status }: { status: WsStatus }) {
+ const b = BADGE[status] || BADGE.draft;
+ return (
+
+ {(RUNNING.includes(status)) && (
+
+ )}
+ {b.label}
+
+ );
+}
+
+/* ββ component ββ */
+
+export default function WorkspacesPage() {
+ const ociCfg = useAppStore((s) => s.ociCfg);
+ const { t } = useI18n();
+
+ const [list, setList] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [filter, setFilter] = useState('all');
+ const [closedGroups, setClosedGroups] = useState>(new Set());
+ const [expanded, setExpanded] = useState(null);
+
+ /* confirm modals */
+ const [confirmDestroy, setConfirmDestroy] = useState(null);
+ const [confirmDelete, setConfirmDelete] = useState(null);
+ const [destroyInput, setDestroyInput] = useState('');
+
+ const pollRef = useRef>(new Set());
+
+ /* ββ load ββ */
+ const load = useCallback(async () => {
+ setLoading(true);
+ try {
+ const ws = await terraformApi.listWorkspaces();
+ setList(ws);
+ } catch {
+ setList([]);
+ }
+ setLoading(false);
+ }, []);
+
+ useEffect(() => { load(); }, [load]);
+
+ /* ββ polling ββ */
+ const pollWs = useCallback(async (wid: string) => {
+ if (pollRef.current.has(wid)) return;
+ pollRef.current.add(wid);
+ for (let i = 0; i < 300; i++) {
+ await new Promise((r) => setTimeout(r, 3000));
+ try {
+ const r = await terraformApi.status(wid);
+ setList((prev) =>
+ prev.map((w) =>
+ w.id === wid
+ ? {
+ ...w,
+ status: r.status,
+ plan_output: r.plan_output || w.plan_output,
+ apply_output: r.apply_output || w.apply_output,
+ destroy_output: r.destroy_output || w.destroy_output,
+ error: r.error || '',
+ }
+ : w,
+ ),
+ );
+ if (!RUNNING.includes(r.status)) {
+ pollRef.current.delete(wid);
+ load();
+ return;
+ }
+ } catch { /* keep polling */ }
+ }
+ pollRef.current.delete(wid);
+ }, [load]);
+
+ /* ββ actions ββ */
+ const doPlan = async (wid: string) => {
+ try { await terraformApi.plan(wid); } catch (e: unknown) { alert('Erro: ' + (e as Error).message); return; }
+ load(); pollWs(wid);
+ };
+ const doApply = async (wid: string) => {
+ try { await terraformApi.apply(wid); } catch (e: unknown) { alert('Erro: ' + (e as Error).message); return; }
+ load(); pollWs(wid);
+ };
+ const doDestroy = async () => {
+ if (!confirmDestroy || destroyInput !== 'DESTROY') return;
+ const wid = confirmDestroy;
+ setConfirmDestroy(null); setDestroyInput('');
+ try { await terraformApi.destroy(wid); } catch (e: unknown) { alert('Erro: ' + (e as Error).message); load(); return; }
+ load(); pollWs(wid);
+ };
+ const doDelete = async () => {
+ if (!confirmDelete) return;
+ const wid = confirmDelete;
+ setConfirmDelete(null);
+ try { await terraformApi.deleteWorkspace(wid); } catch (e: unknown) { alert('Erro: ' + (e as Error).message); }
+ load();
+ };
+ const doCancel = async (wid: string) => {
+ try { await terraformApi.cancel(wid); } catch { /* noop */ }
+ load();
+ };
+
+ /* ββ filter + group ββ */
+ let items = list;
+ if (filter === 'active') items = list.filter((w) => ACTIVE.includes(w.status));
+ else if (filter === 'destroyed') items = list.filter((w) => w.status === 'destroyed');
+ else if (filter === 'failed') items = list.filter((w) => w.status === 'failed');
+ else if (filter === 'draft') items = list.filter((w) => w.status === 'draft');
+
+ const counts = {
+ all: list.length,
+ active: list.filter((w) => ACTIVE.includes(w.status)).length,
+ draft: list.filter((w) => w.status === 'draft').length,
+ failed: list.filter((w) => w.status === 'failed').length,
+ destroyed: list.filter((w) => w.status === 'destroyed').length,
+ };
+
+ const groups: { label: string; date: string; items: Workspace[] }[] = [];
+ const gMap: Record = {};
+ for (const w of items) {
+ const dt = (w.updated_at || w.created_at || '').slice(0, 10);
+ const label = dateLabel(dt);
+ if (gMap[label] === undefined) { gMap[label] = groups.length; groups.push({ label, date: dt, items: [] }); }
+ groups[gMap[label]].items.push(w);
+ }
+
+ const toggleGroup = (label: string) =>
+ setClosedGroups((prev) => {
+ const next = new Set(prev);
+ if (next.has(label)) next.delete(label); else next.add(label);
+ return next;
+ });
+
+ const ociName = (id: string) => {
+ const oc = ociCfg.find((c) => c.id === id);
+ return oc ? (oc.tenancy_name || oc.region || id) : 'β';
+ };
+
+ /* ββ render ββ */
+ return (
+
+ {/* header */}
+
+
+
{t('ws.title')}
+
+
+ {/* filters */}
+
+ {([
+ ['all', t('ws.all')],
+ ['active', t('ws.active')],
+ ['draft', t('ws.draft')],
+ ['failed', t('ws.failed')],
+ ['destroyed', t('ws.destroyed')],
+ ] as [FilterKey, string][]).map(([k, l]) => (
+ setFilter(k)}
+ className="px-2.5 py-1 rounded-full text-[.68rem] font-medium transition-colors cursor-pointer"
+ style={{
+ background: filter === k ? 'var(--pp)' : 'var(--bg3)',
+ color: filter === k ? '#fff' : 'var(--t3)',
+ border: 'none',
+ }}>
+ {l} ({counts[k]})
+
+ ))}
+
+
+
+
+ {loading ? t('ws.loading') : t('ws.refresh')}
+
+
+
+ {/* body */}
+
+ {groups.length === 0 && !loading && (
+
+
+
{t('ws.noWs')}
+
{t('ws.noWsHint')}
+
+ )}
+
+ {groups.length === 0 && loading && (
+
+
+ {t('ws.loadingWs')}
+
+ )}
+
+ {groups.map((g) => {
+ const open = !closedGroups.has(g.label);
+ return (
+
+ {/* group header */}
+
toggleGroup(g.label)}
+ className="flex items-center gap-2 w-full px-3 py-2 rounded-lg cursor-pointer transition-colors"
+ style={{ background: 'var(--bg1)', border: '1px solid var(--bd)' }}>
+
+ {g.label}
+
+ {g.items.length} workspace{g.items.length > 1 ? 's' : ''}
+
+
+
+ {/* group body */}
+ {open && (
+
+ {g.items.map((w) => {
+ const running = RUNNING.includes(w.status);
+ const hasOutput = !!(w.plan_output || w.apply_output || w.destroy_output || w.error);
+ const isExpanded = expanded === w.id;
+ const title = w.session_title || w.name || 'workspace';
+
+ return (
+
+ {/* row */}
+
{
+ if (hasOutput && !running) setExpanded(isExpanded ? null : w.id);
+ }}>
+
+
+
+
+ {title}
+
+
+
+ {ociName(w.oci_config_id)}
+
+ {w.compartment_id && (
+ ...{w.compartment_id.slice(-8)}
+ )}
+
+ {timeAgo(w.updated_at || w.created_at)}
+
+ {hasOutput && !running && (
+
+ {isExpanded ? `βΎ ${t('ws.hideOutput')}` : `βΈ ${t('ws.viewOutput')}`}
+
+ )}
+
+
+
+ {/* actions */}
+
e.stopPropagation()}>
+ {running ? (
+
}
+ label={t('ws.cancel')} onClick={() => doCancel(w.id)} />
+ ) : (
+ <>
+
} label={t('ws.open')}
+ onClick={() => window.open(`/terraform?ws=${w.id}`, '_blank')} />
+
+ {['draft', 'failed', 'destroyed', 'planned', 'applied'].includes(w.status) && (
+
}
+ label={t('ws.plan')} onClick={() => doPlan(w.id)} />
+ )}
+
+ {w.status === 'planned' && (
+
}
+ label={t('ws.apply')} onClick={() => doApply(w.id)} />
+ )}
+
+ {(w.status === 'applied' || w.status === 'planned') && (
+
}
+ label={t('ws.destroy')} onClick={() => { setConfirmDestroy(w.id); setDestroyInput(''); }} />
+ )}
+
+
}
+ label="" title={t('ws.delete')}
+ onClick={() => setConfirmDelete(w.id)} />
+
+
e.stopPropagation()}>
+
+
+ >
+ )}
+
+
+
+ {/* expanded output */}
+ {isExpanded && hasOutput && (
+
+
+ {w.error
+ ? 'ERROR: ' + w.error
+ : (w.destroy_output || w.apply_output || w.plan_output || '')}
+
+
+ )}
+
+ );
+ })}
+
+ )}
+
+ );
+ })}
+
+
+ {/* Destroy confirm modal */}
+ {confirmDestroy && (
+
setConfirmDestroy(null)}>
+ {t('ws.confirmDestroy')}
+
+ {t('ws.confirmDestroyMsg')}
+ {' '}
+ Digite DESTROY para confirmar:
+
+ setDestroyInput(e.target.value)}
+ placeholder="DESTROY"
+ className="w-full px-3 py-2 rounded-lg text-sm text-center"
+ style={{
+ background: 'var(--bg2)', color: 'var(--t1)',
+ border: `1px solid ${destroyInput === 'DESTROY' ? 'var(--gn)' : 'var(--rd)'}`,
+ }}
+ autoFocus
+ onKeyDown={(e) => { if (e.key === 'Enter') doDestroy(); }}
+ />
+
+ setConfirmDestroy(null)} className="btn btn-secondary btn-sm">
+ {t('common.cancel')}
+
+
+ {t('ws.destroy')}
+
+
+
+ )}
+
+ {/* Delete confirm modal */}
+ {confirmDelete && (
+
setConfirmDelete(null)}>
+ {t('ws.deleteWs')}
+
+ {t('ws.deleteWsMsg')}
+
+
+ setConfirmDelete(null)} className="btn btn-secondary btn-sm">
+ {t('common.cancel')}
+
+
+ {t('ws.delete')}
+
+
+
+ )}
+
+ );
+}
+
+/* ββ Small shared components ββ */
+
+function ActionBtn({
+ color, textColor, borderColor, icon, label, title, onClick,
+}: {
+ color: string; textColor?: string; borderColor?: string;
+ icon: React.ReactNode; label: string; title?: string;
+ onClick: () => void;
+}) {
+ return (
+
+ {icon}
+ {label && {label} }
+
+ );
+}
+
+function Modal({ children, onClose }: { children: React.ReactNode; onClose: () => void }) {
+ return (
+
+
e.stopPropagation()}>
+ {children}
+
+
+ );
+}
diff --git a/frontend-react/src/pages/admin/AuditPage.tsx b/frontend-react/src/pages/admin/AuditPage.tsx
new file mode 100644
index 0000000..22cf24b
--- /dev/null
+++ b/frontend-react/src/pages/admin/AuditPage.tsx
@@ -0,0 +1,536 @@
+import { useState, useEffect, useCallback } from 'react';
+import {
+ FileText,
+ ScrollText,
+ MessageSquare,
+ RefreshCw,
+ AlertCircle,
+ Loader2,
+} from 'lucide-react';
+import {
+ adminApi,
+ type AuditEntry,
+ type ConfigLogEntry,
+ type ChatLogEntry,
+} from '@/api/endpoints/admin';
+import { usePolling } from '@/hooks/usePolling';
+import { useI18n } from '@/i18n';
+
+/* ββ Helpers ββ */
+
+function formatDate(iso: string) {
+ if (!iso) return 'β';
+ try {
+ const d = new Date(iso.includes('T') ? iso : iso.replace(' ', 'T') + 'Z');
+ return d.toLocaleString('pt-BR', {
+ day: '2-digit', month: '2-digit', year: '2-digit',
+ hour: '2-digit', minute: '2-digit', second: '2-digit',
+ });
+ } catch {
+ return iso;
+ }
+}
+
+function SeverityBadge({ severity }: { severity: string }) {
+ const map: Record = {
+ error: { bg: 'var(--rdl)', fg: 'var(--rd)' },
+ success: { bg: 'var(--gnl)', fg: 'var(--gn)' },
+ info: { bg: 'var(--bll)', fg: 'var(--bl)' },
+ warning: { bg: 'var(--yll)', fg: 'var(--yl)' },
+ };
+ const s = map[severity] ?? { bg: 'var(--bg3)', fg: 'var(--t3)' };
+ return (
+
+ {severity}
+
+ );
+}
+
+function ActionTag({ action }: { action: string }) {
+ return (
+
+ {action}
+
+ );
+}
+
+function EmptyState({ text }: { text: string }) {
+ return (
+
+ );
+}
+
+function ErrorBanner({ message }: { message: string }) {
+ return (
+
+ );
+}
+
+function LoadingRow({ cols, text }: { cols: number; text: string }) {
+ return (
+
+
+
+ {text}
+
+
+ );
+}
+
+/* ββ Shared table wrapper ββ */
+
+const tableContainerStyle: React.CSSProperties = {
+ background: 'var(--bg1)',
+ border: '1px solid var(--bd)',
+ borderRadius: 'var(--r)',
+ overflow: 'hidden',
+};
+
+const thStyle: React.CSSProperties = {
+ padding: '0.6rem 0.75rem',
+ fontSize: '0.7rem',
+ fontWeight: 600,
+ textTransform: 'uppercase',
+ letterSpacing: '0.04em',
+ color: 'var(--t3)',
+ background: 'var(--bg2)',
+ borderBottom: '1px solid var(--bd)',
+ whiteSpace: 'nowrap',
+};
+
+const tdStyle: React.CSSProperties = {
+ padding: '0.55rem 0.75rem',
+ fontSize: '0.76rem',
+ borderBottom: '1px solid var(--bg3)',
+ color: 'var(--t2)',
+};
+
+const tdDateStyle: React.CSSProperties = {
+ ...tdStyle,
+ fontSize: '0.68rem',
+ color: 'var(--t4)',
+ whiteSpace: 'nowrap',
+ fontFamily: 'var(--fm)',
+};
+
+const tdMonoStyle: React.CSSProperties = {
+ ...tdStyle,
+ fontSize: '0.68rem',
+ color: 'var(--t4)',
+ fontFamily: 'var(--fm)',
+};
+
+/* ββ Toolbar ββ */
+
+interface ToolbarProps {
+ limit: number;
+ onLimitChange: (n: number) => void;
+ severity?: string;
+ onSeverityChange?: (s: string) => void;
+ onRefresh: () => void;
+ loading: boolean;
+ showSeverity?: boolean;
+}
+
+function Toolbar({
+ limit,
+ onLimitChange,
+ severity,
+ onSeverityChange,
+ onRefresh,
+ loading,
+ showSeverity = false,
+}: ToolbarProps) {
+ const { t } = useI18n();
+ const selectStyle: React.CSSProperties = {
+ fontSize: '0.72rem',
+ padding: '0.3rem 0.5rem',
+ borderRadius: '8px',
+ border: '1px solid var(--bd)',
+ background: 'var(--bg2)',
+ color: 'var(--t2)',
+ outline: 'none',
+ };
+
+ return (
+
+ {showSeverity && onSeverityChange && (
+ onSeverityChange(e.target.value)}
+ style={selectStyle}
+ >
+ {t('audit.all')}
+ {t('audit.errors')}
+ {t('audit.success')}
+ {t('audit.info')}
+
+ )}
+ onLimitChange(Number(e.target.value))}
+ style={selectStyle}
+ >
+ 50 {t('audit.records')}
+ 100 {t('audit.records')}
+ 200 {t('audit.records')}
+ 500 {t('audit.records')}
+
+
+
+ {t('audit.refresh')}
+
+
+ );
+}
+
+/* ββ Tab definitions ββ */
+
+type TabId = 'audit' | 'config' | 'chat';
+
+/* ββ Audit Log Table ββ */
+
+function AuditLogTable() {
+ const { t } = useI18n();
+ const [data, setData] = useState([]);
+ const [limit, setLimit] = useState(100);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
+
+ const load = useCallback(async () => {
+ setLoading(true);
+ setError('');
+ try {
+ const rows = await adminApi.getAuditLog(limit);
+ setData(rows);
+ } catch (e: any) {
+ setError(e.message || 'Erro ao carregar audit log');
+ } finally {
+ setLoading(false);
+ }
+ }, [limit]);
+
+ useEffect(() => { load(); }, [load]);
+
+ usePolling(async () => { await load(); }, 30_000, true);
+
+ return (
+
+
+
+ {data.length > 0 && `${data.length} ${t('audit.records')}`}
+
+
+
+ {error &&
}
+
+
+
+
+ {t('audit.date')}
+ {t('audit.user')}
+ {t('audit.action')}
+ {t('audit.resource')}
+ {t('audit.details')}
+ {t('audit.ip')}
+
+
+
+ {loading && data.length === 0 ? (
+
+ ) : data.length === 0 ? (
+
+
+
+ ) : (
+ data.map((row) => (
+
+ {formatDate(row.created_at)}
+ {row.username || 'β'}
+
+
+ {row.resource ? (row.resource.length > 20 ? row.resource.slice(0, 20) + '...' : row.resource) : 'β'}
+
+
+ {row.details?.slice(0, 120) || 'β'}
+
+ {row.ip || 'β'}
+
+ ))
+ )}
+
+
+
+
+ );
+}
+
+/* ββ Config Logs Table ββ */
+
+function ConfigLogsTable() {
+ const { t } = useI18n();
+ const [data, setData] = useState([]);
+ const [limit, setLimit] = useState(50);
+ const [severity, setSeverity] = useState('');
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
+
+ const load = useCallback(async () => {
+ setLoading(true);
+ setError('');
+ try {
+ const rows = await adminApi.getConfigLogs({
+ severity: severity || undefined,
+ limit,
+ });
+ setData(rows);
+ } catch (e: any) {
+ setError(e.message || 'Erro ao carregar config logs');
+ } finally {
+ setLoading(false);
+ }
+ }, [limit, severity]);
+
+ useEffect(() => { load(); }, [load]);
+
+ usePolling(async () => { await load(); }, 30_000, true);
+
+ return (
+
+
+
+ {data.length > 0 && `${data.length} ${t('audit.records')}`}
+
+
+
+ {error &&
}
+
+
+
+
+ {t('audit.date')}
+ {t('audit.config')}
+ {t('audit.action')}
+ {t('audit.status')}
+ {t('audit.message')}
+
+
+
+ {loading && data.length === 0 ? (
+
+ ) : data.length === 0 ? (
+
+
+
+ ) : (
+ data.map((row) => (
+
+ {formatDate(row.created_at)}
+
+ {row.config_name || row.config_id?.slice(0, 8) || 'β'}
+
+
+
+
+ {row.message?.slice(0, 150) || 'β'}
+
+
+ ))
+ )}
+
+
+
+
+ );
+}
+
+/* ββ Chat Logs Table ββ */
+
+function ChatLogsTable() {
+ const { t } = useI18n();
+ const [data, setData] = useState([]);
+ const [limit, setLimit] = useState(50);
+ const [severity, setSeverity] = useState('');
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
+
+ const load = useCallback(async () => {
+ setLoading(true);
+ setError('');
+ try {
+ const rows = await adminApi.getChatLogs({
+ severity: severity || undefined,
+ limit,
+ });
+ setData(rows);
+ } catch (e: any) {
+ setError(e.message || 'Erro ao carregar chat logs');
+ } finally {
+ setLoading(false);
+ }
+ }, [limit, severity]);
+
+ useEffect(() => { load(); }, [load]);
+
+ usePolling(async () => { await load(); }, 30_000, true);
+
+ return (
+
+
+
+ {data.length > 0 && `${data.length} ${t('audit.records')}`}
+
+
+
+ {error &&
}
+
+
+
+
+ {t('audit.date')}
+ {t('audit.session')}
+ {t('audit.source')}
+ {t('audit.action')}
+ {t('audit.status')}
+ {t('audit.message')}
+
+
+
+ {loading && data.length === 0 ? (
+
+ ) : data.length === 0 ? (
+
+
+
+ ) : (
+ data.map((row) => (
+
+ {formatDate(row.created_at)}
+
+ {row.session_id ? row.session_id.slice(0, 8) + '...' : 'β'}
+
+ {row.source || 'β'}
+
+
+
+ {row.message?.slice(0, 150) || 'β'}
+
+
+ ))
+ )}
+
+
+
+
+ );
+}
+
+/* ββ Main Page ββ */
+
+export default function AuditPage() {
+ const { t } = useI18n();
+ const [activeTab, setActiveTab] = useState('audit');
+
+ const tabs: { id: TabId; label: string; icon: React.ReactNode }[] = [
+ { id: 'audit', label: t('audit.tabAudit'), icon: },
+ { id: 'config', label: t('audit.tabConfig'), icon: },
+ { id: 'chat', label: t('audit.tabChat'), icon: },
+ ];
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
{t('audit.title')}
+
+ {t('audit.subtitle')}
+
+
+
+
+ {/* Card with tabs + content */}
+
+ {/* Tab bar */}
+
+ {tabs.map((tab) => (
+ setActiveTab(tab.id)}
+ className="btn btn-sm"
+ style={{
+ background: activeTab === tab.id ? 'var(--bg1)' : 'transparent',
+ color: activeTab === tab.id ? 'var(--t1)' : 'var(--t3)',
+ border: activeTab === tab.id ? '1px solid var(--bd)' : '1px solid transparent',
+ boxShadow: activeTab === tab.id ? 'var(--sh1)' : 'none',
+ }}
+ >
+ {tab.icon}
+ {tab.label}
+
+ ))}
+
+
+ {/* Tab content */}
+
+ {activeTab === 'audit' &&
}
+ {activeTab === 'config' &&
}
+ {activeTab === 'chat' &&
}
+
+
+
+ );
+}
diff --git a/frontend-react/src/pages/admin/MfaPage.tsx b/frontend-react/src/pages/admin/MfaPage.tsx
new file mode 100644
index 0000000..0167209
--- /dev/null
+++ b/frontend-react/src/pages/admin/MfaPage.tsx
@@ -0,0 +1,281 @@
+import { useState, useCallback } from 'react';
+import { Lock, ShieldCheck, ShieldOff, KeyRound, Loader2, CheckCircle2, AlertCircle } from 'lucide-react';
+import { useAuthStore } from '@/stores/auth';
+import client from '@/api/client';
+import { useI18n } from '@/i18n';
+
+interface MfaSetupResponse {
+ secret: string;
+ uri: string;
+}
+
+export default function MfaPage() {
+ const { user, checkAuth } = useAuthStore();
+ const { t } = useI18n();
+
+ const [secret, setSecret] = useState(null);
+ const [totpUri, setTotpUri] = useState(null);
+ const [code, setCode] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [msg, setMsg] = useState<{ text: string; type: 's' | 'e' | 'i' } | null>(null);
+
+ const mfaEnabled = !!user?.mfa_enabled;
+
+ const handleSetup = useCallback(async () => {
+ setLoading(true);
+ setMsg(null);
+ try {
+ const data = await client.post('/mfa/setup') as unknown as MfaSetupResponse;
+ setSecret(data.secret);
+ setTotpUri(data.uri);
+ } catch (err) {
+ setMsg({ text: err instanceof Error ? err.message : 'Erro ao gerar secret', type: 'e' });
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ const handleVerify = useCallback(async () => {
+ if (!code || code.length !== 6) {
+ setMsg({ text: t('mfa.invalidCode'), type: 'e' });
+ return;
+ }
+ setLoading(true);
+ setMsg(null);
+ try {
+ await client.post('/mfa/verify', { totp_code: code });
+ setMsg({ text: t('mfa.activated'), type: 's' });
+ setSecret(null);
+ setTotpUri(null);
+ setCode('');
+ await checkAuth();
+ } catch (err) {
+ setMsg({ text: err instanceof Error ? err.message : 'Codigo invalido', type: 'e' });
+ } finally {
+ setLoading(false);
+ }
+ }, [code, checkAuth, t]);
+
+ const handleDisable = useCallback(async () => {
+ if (!user) return;
+ if (!window.confirm(t('mfa.confirmDisable'))) return;
+ setLoading(true);
+ setMsg(null);
+ try {
+ await client.post(`/mfa/disable/${user.id}`);
+ setMsg({ text: t('mfa.deactivated'), type: 'i' });
+ await checkAuth();
+ } catch (err) {
+ setMsg({ text: err instanceof Error ? err.message : 'Erro ao desativar MFA', type: 'e' });
+ } finally {
+ setLoading(false);
+ }
+ }, [user, checkAuth, t]);
+
+ const alertStyles: Record = {
+ s: { bg: 'var(--gnl)', color: 'var(--gn)', icon: CheckCircle2 },
+ e: { bg: 'var(--rdl)', color: 'var(--rd)', icon: AlertCircle },
+ i: { bg: 'color-mix(in srgb, var(--bl) 12%, transparent)', color: 'var(--bl)', icon: AlertCircle },
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
{t('mfa.title')}
+
+ {t('mfa.subtitle')}
+
+
+
+
+ {/* Card */}
+
+ {/* Status banner */}
+
+ {mfaEnabled ? (
+
+ ) : (
+
+ )}
+
+
+ {mfaEnabled ? t('mfa.enabled') : t('mfa.disabled')}
+
+
+ {mfaEnabled
+ ? t('mfa.enabledDesc')
+ : t('mfa.disabledDesc')}
+
+
+
+
+ {/* Content */}
+
+ {/* Message */}
+ {msg && (() => {
+ const st = alertStyles[msg.type];
+ const Icon = st.icon;
+ return (
+
+
+ {msg.text}
+
+ );
+ })()}
+
+ {mfaEnabled ? (
+ /* Enabled state: show disable button */
+
+
+
+
+
+ {t('mfa.disableHint')}
+
+
+ {loading ? : }
+ {t('mfa.disableBtn')}
+
+
+ ) : secret && totpUri ? (
+ /* Setup step 2: show secret + QR + verify */
+
+ {/* Instructions */}
+
+
+ 1
+
+
+
+ {t('mfa.scanQr')}
+
+
+ {t('mfa.openApp')}
+
+
+
+
+ {/* QR code */}
+
+
+
+
+
+
+ {/* Secret */}
+
+ SECRET
+
+ {secret}
+
+
+
+ {/* Verify */}
+
+
+ 2
+
+
+
+ {t('mfa.enterCode')}
+
+
+ setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
+ placeholder="000000"
+ maxLength={6}
+ className="flex-1 px-3 py-2 rounded-lg text-sm text-center tracking-[.3em] font-semibold outline-none transition-colors"
+ style={{
+ background: 'var(--bg2)',
+ border: '1px solid var(--bd)',
+ color: 'var(--t1)',
+ fontFamily: 'var(--fm)',
+ }}
+ onFocus={(e) => { e.target.style.borderColor = 'var(--ac)'; }}
+ onBlur={(e) => { e.target.style.borderColor = 'var(--bd)'; }}
+ onKeyDown={(e) => { if (e.key === 'Enter') handleVerify(); }}
+ autoFocus
+ />
+
+ {loading ? : }
+ {t('mfa.activateMfa')}
+
+
+
+
+
+ ) : (
+ /* Setup step 1: generate secret */
+
+
+
+
+
+ {t('mfa.generateHint')}
+
+
+ {loading ? : }
+ {t('mfa.generateSecret')}
+
+
+ )}
+
+
+
+ );
+}
diff --git a/frontend-react/src/pages/admin/UsersPage.tsx b/frontend-react/src/pages/admin/UsersPage.tsx
new file mode 100644
index 0000000..d2fa7e1
--- /dev/null
+++ b/frontend-react/src/pages/admin/UsersPage.tsx
@@ -0,0 +1,463 @@
+import { useState, useEffect, useCallback } from 'react';
+import { Users, Plus, Shield, Eye, User as UserIcon, Trash2, X, AlertTriangle, CheckCircle, Loader2, UserPlus } from 'lucide-react';
+import client from '@/api/client';
+import { useI18n } from '@/i18n';
+
+/* ββ Types ββ */
+interface AppUser {
+ id: string;
+ first_name: string | null;
+ last_name: string | null;
+ username: string;
+ email: string | null;
+ role: 'admin' | 'user' | 'viewer';
+ mfa_enabled: boolean | number;
+ is_active: boolean | number;
+ created_at: string | null;
+ last_login: string | null;
+}
+
+type Role = 'admin' | 'user' | 'viewer';
+
+const ROLE_CONFIG: Record = {
+ admin: { label: 'Admin', color: 'var(--rd)', bg: 'var(--rdl)', icon: Shield },
+ user: { label: 'User', color: 'var(--bl)', bg: 'var(--bll)', icon: UserIcon },
+ viewer: { label: 'Viewer', color: 'var(--t3)', bg: 'var(--bg3)', icon: Eye },
+};
+
+/* ββ Component ββ */
+export default function UsersPage() {
+ const { t } = useI18n();
+ const [users, setUsers] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [showForm, setShowForm] = useState(false);
+ const [confirmDelete, setConfirmDelete] = useState(null);
+ const [msg, setMsg] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
+ const [submitting, setSubmitting] = useState(false);
+
+ // Form state
+ const [firstName, setFirstName] = useState('');
+ const [lastName, setLastName] = useState('');
+ const [username, setUsername] = useState('');
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [role, setRole] = useState('viewer');
+
+ const fetchUsers = useCallback(async () => {
+ try {
+ const data = await client.get('/users') as unknown as AppUser[];
+ setUsers(data);
+ } catch (err) {
+ setMsg({ text: err instanceof Error ? err.message : 'Erro ao carregar usuarios', type: 'error' });
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => { fetchUsers(); }, [fetchUsers]);
+
+ useEffect(() => {
+ if (msg) {
+ const timer = setTimeout(() => setMsg(null), 4000);
+ return () => clearTimeout(timer);
+ }
+ }, [msg]);
+
+ const resetForm = () => {
+ setFirstName(''); setLastName(''); setUsername('');
+ setEmail(''); setPassword(''); setRole('viewer');
+ };
+
+ const handleCreate = async () => {
+ if (!firstName.trim() || !lastName.trim()) {
+ setMsg({ text: t('usr.requiredName'), type: 'error' });
+ return;
+ }
+ if (!username.trim()) {
+ setMsg({ text: t('usr.requiredUsername'), type: 'error' });
+ return;
+ }
+ if (!password) {
+ setMsg({ text: t('usr.requiredPassword'), type: 'error' });
+ return;
+ }
+
+ setSubmitting(true);
+ try {
+ await client.post('/auth/register', {
+ first_name: firstName.trim(),
+ last_name: lastName.trim(),
+ username: username.trim(),
+ email: email.trim() || undefined,
+ password,
+ role,
+ });
+ setMsg({ text: t('usr.created'), type: 'success' });
+ resetForm();
+ setShowForm(false);
+ await fetchUsers();
+ } catch (err) {
+ setMsg({ text: err instanceof Error ? err.message : 'Erro ao criar usuario', type: 'error' });
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const handleRoleChange = async (userId: string, newRole: Role) => {
+ try {
+ await client.put(`/users/${userId}`, { role: newRole });
+ setUsers(prev => prev.map(u => u.id === userId ? { ...u, role: newRole } : u));
+ setMsg({ text: t('usr.roleUpdated'), type: 'success' });
+ } catch (err) {
+ setMsg({ text: err instanceof Error ? err.message : 'Erro ao atualizar role', type: 'error' });
+ }
+ };
+
+ const handleDeactivate = async (userId: string) => {
+ try {
+ await client.delete(`/users/${userId}`);
+ setConfirmDelete(null);
+ setMsg({ text: t('usr.deactivated'), type: 'success' });
+ await fetchUsers();
+ } catch (err) {
+ setMsg({ text: err instanceof Error ? err.message : 'Erro ao desativar usuario', type: 'error' });
+ }
+ };
+
+ const formatDate = (d: string | null) => {
+ if (!d) return '---';
+ try {
+ return new Intl.DateTimeFormat('pt-BR', {
+ day: '2-digit', month: '2-digit', year: 'numeric',
+ hour: '2-digit', minute: '2-digit',
+ }).format(new Date(d));
+ } catch {
+ return d;
+ }
+ };
+
+ const activeUsers = users.filter(u => u.is_active);
+ const inactiveUsers = users.filter(u => !u.is_active);
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Toast */}
+ {msg && (
+
+ {msg.type === 'success' ?
:
}
+ {msg.text}
+
+ )}
+
+ {/* Header */}
+
+
+
+
+
+
{t('usr.title')}
+
+ {activeUsers.length} {activeUsers.length !== 1 ? t('usr.activesCount') : t('usr.activeCount')}
+ {inactiveUsers.length > 0 && ` Β· ${inactiveUsers.length} ${inactiveUsers.length !== 1 ? t('usr.inactivesCount') : t('usr.inactiveCount')}`}
+
+
+
+
{users.length} {t('usr.total')}
+
setShowForm(!showForm)}
+ className="btn btn-primary"
+ >
+ {showForm ? : }
+ {showForm ? t('usr.cancel') : t('usr.newUser')}
+
+
+
+ {/* Create User Form */}
+ {showForm && (
+
+
+
+
+
+
{t('usr.createUser')}
+
+
+
+
+
+
+ {t('usr.email')}
+ setEmail(e.target.value)}
+ />
+
+
+ {t('usr.password')}
+ setPassword(e.target.value)}
+ />
+
+
+
{t('usr.role')}
+
+ {(['viewer', 'user', 'admin'] as Role[]).map(r => {
+ const cfg = ROLE_CONFIG[r];
+ const Icon = cfg.icon;
+ const selected = role === r;
+ return (
+ setRole(r)}
+ className="flex-1 flex items-center justify-center gap-1.5 py-2 rounded-lg text-xs font-semibold transition-all duration-200"
+ style={{
+ background: selected ? cfg.bg : 'var(--bg2)',
+ border: `1.5px solid ${selected ? cfg.color : 'var(--bd)'}`,
+ color: selected ? cfg.color : 'var(--t3)',
+ }}
+ >
+
+ {cfg.label}
+
+ );
+ })}
+
+
+
+
+
+ {submitting ? : }
+ {submitting ? t('usr.creating') : t('usr.create')}
+
+
+ )}
+
+ {/* Users Table */}
+ {users.length === 0 ? (
+
+
+
+
{t('usr.noUsers')}
+
{t('usr.noUsersHint')}
+
+
+ ) : (
+
+
+
+
+
+ {t('usr.name')}
+ {t('usr.username')}
+ {t('usr.email')}
+ {t('usr.role')}
+ {t('usr.mfa')}
+ {t('usr.status')}
+ {t('usr.lastLogin')}
+ {t('usr.actions')}
+
+
+
+ {users.map((u, idx) => {
+ const cfg = ROLE_CONFIG[u.role];
+ const isActive = !!u.is_active;
+ const isAdmin = u.username === 'admin';
+ const RoleIcon = cfg.icon;
+
+ return (
+ { e.currentTarget.style.background = 'var(--bg2)'; }}
+ onMouseLeave={e => { e.currentTarget.style.background = ''; }}
+ >
+ {/* Name */}
+
+
+ {u.first_name || ''} {u.last_name || ''}
+
+
+
+ {/* Username */}
+
+
+ {u.username}
+
+
+
+ {/* Email */}
+
+
+ {u.email || '---'}
+
+
+
+ {/* Role Badge */}
+
+
+
+ {cfg.label}
+
+
+
+ {/* MFA */}
+
+ {u.mfa_enabled ? (
+
+ {t('usr.active')}
+
+ ) : (
+ ---
+ )}
+
+
+ {/* Status */}
+
+
+ {isActive ? t('usr.active') : t('usr.inactive')}
+
+
+
+ {/* Last login */}
+
+
+ {u.last_login ? formatDate(u.last_login) : t('usr.never')}
+
+
+
+ {/* Actions */}
+
+
+ {/* Role selector */}
+
handleRoleChange(u.id, e.target.value as Role)}
+ disabled={isAdmin}
+ className="px-2 py-1 rounded-lg text-xs outline-none cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
+ style={{
+ background: 'var(--bg2)',
+ border: '1px solid var(--bd)',
+ color: 'var(--t2)',
+ }}
+ >
+ Admin
+ User
+ Viewer
+
+
+ {/* Deactivate */}
+ {!isAdmin && isActive && (
+ <>
+ {confirmDelete === u.id ? (
+
+ handleDeactivate(u.id)}
+ className="btn btn-sm" style={{ background: 'var(--rd)', color: '#fff' }}
+ >
+ {t('usr.confirm')}
+
+ setConfirmDelete(null)}
+ className="btn btn-secondary btn-sm"
+ >
+ {t('common.no')}
+
+
+ ) : (
+
setConfirmDelete(u.id)}
+ className="p-1.5 rounded-lg transition-colors"
+ style={{ color: 'var(--t4)' }}
+ onMouseEnter={e => { e.currentTarget.style.background = 'var(--rdl)'; e.currentTarget.style.color = 'var(--rd)'; }}
+ onMouseLeave={e => { e.currentTarget.style.background = ''; e.currentTarget.style.color = 'var(--t4)'; }}
+ title={t('usr.deactivate')}
+ >
+
+
+ )}
+ >
+ )}
+
+
+
+ );
+ })}
+
+
+
+
+ )}
+
+ );
+}
diff --git a/frontend-react/src/pages/config/AdbConfigPage.tsx b/frontend-react/src/pages/config/AdbConfigPage.tsx
new file mode 100644
index 0000000..5ca8bfa
--- /dev/null
+++ b/frontend-react/src/pages/config/AdbConfigPage.tsx
@@ -0,0 +1,644 @@
+import { useState, useEffect, useCallback, useRef } from 'react';
+import { useAppStore } from '@/stores/app';
+import { useI18n } from '@/i18n';
+import { adbApi, type AdbConfigFull } from '@/api/endpoints/adb';
+import {
+ Database, Plus, Pencil, Trash2, FlaskConical, Check, X, Loader2,
+ ChevronRight, Upload, Shield, Table2, ChevronDown, ChevronUp, Search,
+} from 'lucide-react';
+
+/* ββ Msg ββ */
+function Msg({ type, text }: { type: 's' | 'e' | 'i'; text: string }) {
+ const bg = type === 's' ? 'var(--gnl)' : type === 'e' ? 'var(--rdl)' : 'var(--bg2)';
+ const fg = type === 's' ? 'var(--gn)' : type === 'e' ? 'var(--rd)' : 'var(--t2)';
+ return (
+
+ {type === 's' && }
+ {type === 'e' && }
+ {type === 'i' && }
+
+
+ );
+}
+
+/* ββ Main ββ */
+export default function AdbConfigPage() {
+ const { genaiCfg, embModels } = useAppStore();
+ const { t } = useI18n();
+
+ const [configs, setConfigs] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ // Form
+ const [formOpen, setFormOpen] = useState(false);
+ const [editing, setEditing] = useState(null);
+ const [saving, setSaving] = useState(false);
+ const [formMsg, setFormMsg] = useState<{ type: 's' | 'e' | 'i'; text: string } | null>(null);
+ const formRef = useRef(null);
+ const walletRef = useRef(null);
+
+ // Form fields
+ const [cfgName, setCfgName] = useState('');
+ const [dsn, setDsn] = useState('');
+ const [dsnOptions, setDsnOptions] = useState([]);
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [walletPassword, setWalletPassword] = useState('');
+ const [genaiConfigId, setGenaiConfigId] = useState('');
+ const [embeddingModelId, setEmbeddingModelId] = useState('cohere.embed-v4.0');
+ const [parsingWallet, setParsingWallet] = useState(false);
+
+ // Table
+ const [tableMsg, setTableMsg] = useState<{ type: 's' | 'e' | 'i'; text: string } | null>(null);
+ const [testResults, setTestResults] = useState>({});
+ const [testingMap, setTestingMap] = useState>({});
+ const [confirmDelete, setConfirmDelete] = useState(null);
+ const [expandedTables, setExpandedTables] = useState>({});
+
+ // Wallet upload section
+ const walletUploadRef = useRef(null);
+ const [walletUploadId, setWalletUploadId] = useState('');
+ const [uploadingWallet, setUploadingWallet] = useState(false);
+ const [walletMsg, setWalletMsg] = useState<{ type: 's' | 'e' | 'i'; text: string } | null>(null);
+
+ // New table form
+ const [newTableName, setNewTableName] = useState('');
+ const [newTableDesc, setNewTableDesc] = useState('');
+
+ const fetchConfigs = useCallback(async () => {
+ try {
+ const data = await adbApi.list();
+ setConfigs(data);
+ } catch {} finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => { fetchConfigs(); }, [fetchConfigs]);
+
+ const resetForm = () => {
+ setEditing(null);
+ setFormOpen(false);
+ setCfgName('');
+ setDsn('');
+ setDsnOptions([]);
+ setUsername('');
+ setPassword('');
+ setWalletPassword('');
+ setGenaiConfigId('');
+ setEmbeddingModelId('cohere.embed-v4.0');
+ setFormMsg(null);
+ setSaving(false);
+ if (walletRef.current) walletRef.current.value = '';
+ };
+
+ const startEdit = (cfg: AdbConfigFull) => {
+ setEditing(cfg);
+ setCfgName(cfg.config_name);
+ setDsn(cfg.dsn);
+ setDsnOptions([]);
+ setUsername(cfg.username);
+ setPassword('');
+ setWalletPassword('');
+ setGenaiConfigId(cfg.genai_config_id || '');
+ setEmbeddingModelId(cfg.embedding_model_id || 'cohere.embed-v4.0');
+ setFormOpen(true);
+ setFormMsg(null);
+ setTimeout(() => formRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 100);
+ };
+
+ const parseWallet = async () => {
+ const file = walletRef.current?.files?.[0];
+ if (!file) { setFormMsg({ type: 'e', text: t('adb.selectWallet') }); return; }
+ setParsingWallet(true);
+ try {
+ const result = await adbApi.parseWallet(file);
+ setDsnOptions(result.dsn_names);
+ if (result.dsn_names.length > 0) setDsn(result.dsn_names[0]);
+ setFormMsg({ type: 's', text: `Wallet analisado! ${result.dsn_names.length} DSN(s): ${result.dsn_names.join(', ')} . Arquivos: ${result.files.join(', ')}` });
+ } catch (err) {
+ setFormMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro ao analisar wallet' });
+ } finally {
+ setParsingWallet(false);
+ }
+ };
+
+ const handleSave = async () => {
+ if (!cfgName.trim()) { setFormMsg({ type: 'e', text: t('adb.fillName') }); return; }
+ if (!dsn.trim()) { setFormMsg({ type: 'e', text: t('adb.fillDsn') }); return; }
+ if (!username.trim()) { setFormMsg({ type: 'e', text: t('adb.fillUsername') }); return; }
+ if (!editing && !password) { setFormMsg({ type: 'e', text: t('adb.fillPassword') }); return; }
+
+ const fd = new FormData();
+ fd.append('config_name', cfgName.trim());
+ fd.append('dsn', dsn.trim());
+ fd.append('username', username.trim());
+ fd.append('password', password);
+ fd.append('wallet_password', walletPassword);
+ fd.append('use_mtls', 'true');
+ fd.append('genai_config_id', genaiConfigId);
+ fd.append('embedding_model_id', embeddingModelId);
+ const walletFile = walletRef.current?.files?.[0];
+ if (walletFile) fd.append('wallet', walletFile);
+
+ setSaving(true);
+ setFormMsg({ type: 'i', text: t('adb.saving') });
+
+ try {
+ if (editing) {
+ await adbApi.update(editing.id, fd);
+ } else {
+ await adbApi.create(fd);
+ }
+ setFormMsg({ type: 's', text: `Conexao ${editing ? 'atualizada' : 'salva'}!${walletFile ? ' Wallet incluido.' : ''}` });
+ await fetchConfigs();
+ useAppStore.getState().loadData();
+ setTimeout(resetForm, 1200);
+ } catch (err) {
+ setFormMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro ao salvar' });
+ setSaving(false);
+ }
+ };
+
+ const handleDelete = async (id: string) => {
+ setConfirmDelete(null);
+ setTableMsg({ type: 'i', text: t('adb.deleting') });
+ try {
+ await adbApi.remove(id);
+ await fetchConfigs();
+ useAppStore.getState().loadData();
+ setTableMsg({ type: 's', text: 'Conexao excluida com sucesso!' });
+ if (editing?.id === id) resetForm();
+ setTimeout(() => setTableMsg(null), 3000);
+ } catch (err) {
+ setTableMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro ao excluir' });
+ }
+ };
+
+ const handleTest = async (id: string) => {
+ setTestingMap((p) => ({ ...p, [id]: true }));
+ setTestResults((p) => { const n = { ...p }; delete n[id]; return n; });
+ try {
+ const res = await adbApi.test(id);
+ setTestResults((p) => ({
+ ...p,
+ [id]: { type: res.status === 'success' ? 's' : 'e', text: res.message },
+ }));
+ } catch (err) {
+ setTestResults((p) => ({ ...p, [id]: { type: 'e', text: err instanceof Error ? err.message : 'Erro' } }));
+ } finally {
+ setTestingMap((p) => ({ ...p, [id]: false }));
+ }
+ };
+
+ const handleWalletUpload = async () => {
+ const file = walletUploadRef.current?.files?.[0];
+ if (!file) { setWalletMsg({ type: 'e', text: t('adb.selectWallet') }); return; }
+ if (!walletUploadId) { setWalletMsg({ type: 'e', text: t('adb.selectConnection') }); return; }
+ setUploadingWallet(true);
+ setWalletMsg({ type: 'i', text: 'Enviando wallet...' });
+ try {
+ const res = await adbApi.uploadWallet(walletUploadId, file);
+ await fetchConfigs();
+ useAppStore.getState().loadData();
+ setWalletMsg({ type: 's', text: `Wallet enviada! Arquivos: ${res.files.join(', ')}` });
+ if (walletUploadRef.current) walletUploadRef.current.value = '';
+ } catch (err) {
+ setWalletMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro ao enviar wallet' });
+ } finally {
+ setUploadingWallet(false);
+ }
+ };
+
+ const handleAddTable = async (vid: string) => {
+ if (!newTableName.trim()) { setTableMsg({ type: 'e', text: 'Digite o nome da tabela.' }); return; }
+ try {
+ await adbApi.addTable(vid, newTableName.trim(), newTableDesc.trim());
+ await fetchConfigs();
+ useAppStore.getState().loadData();
+ setTableMsg({ type: 's', text: `Tabela ${newTableName.trim()} registrada!` });
+ setNewTableName('');
+ setNewTableDesc('');
+ setTimeout(() => setTableMsg(null), 3000);
+ } catch (err) {
+ setTableMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro' });
+ }
+ };
+
+ const handleToggleTable = async (vid: string, tid: string, currentActive: boolean) => {
+ try {
+ await adbApi.updateTable(vid, tid, { is_active: !currentActive });
+ await fetchConfigs();
+ useAppStore.getState().loadData();
+ } catch (err) {
+ setTableMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro' });
+ }
+ };
+
+ const handleRemoveTable = async (vid: string, tid: string) => {
+ if (!confirm('Excluir esta tabela do registro?')) return;
+ try {
+ await adbApi.removeTable(vid, tid);
+ await fetchConfigs();
+ useAppStore.getState().loadData();
+ setTableMsg({ type: 's', text: 'Tabela removida.' });
+ setTimeout(() => setTableMsg(null), 3000);
+ } catch (err) {
+ setTableMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro' });
+ }
+ };
+
+ const isFormVisible = formOpen || !!editing;
+ const inputCls = "w-full px-3 py-2 rounded-lg text-[.78rem] outline-none transition-colors focus:ring-1";
+ const inputStyle = { background: 'var(--bg2)', border: '1.5px solid var(--bd)', color: 'var(--t1)' };
+ const labelCls = "block";
+ const labelStyle = {};
+ const hintCls = "text-[.64rem] mt-0.5";
+ const hintStyle = { color: 'var(--t4)' };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
+
{t('adb.title')}
+
{t('adb.subtitle')}
+
+
+
+ {configs.length} {configs.length === 1 ? t('adb.connection') : t('adb.connections')}
+
+
+
+ {/* Table */}
+
+
+
+
+
+
{t('adb.registered')}
+
+
+ {tableMsg &&
}
+
+ {loading ? (
+
+
+
+ ) : configs.length === 0 ? (
+
+
+
{t('adb.noConnections')}
+
+ ) : (
+
+
+
+
+ {['Nome', 'DSN', 'User', 'Tabelas', 'mTLS', 'Wallet', 'Embed', 'Acoes'].map((h) => (
+ {h}
+ ))}
+
+
+
+ {configs.map((c) => {
+ const tables = c.tables || [];
+ const emb = c.embedding_model_id ? embModels[c.embedding_model_id] : null;
+ const isExpanded = expandedTables[c.id];
+ return (
+ { if (editing?.id !== c.id) e.currentTarget.style.background = 'var(--bg2)'; }}
+ onMouseLeave={(e) => { if (editing?.id !== c.id) e.currentTarget.style.background = ''; }}
+ >
+
+ {c.config_name}
+
+
+ {c.dsn}
+
+
+ {c.username}
+
+
+ {tables.length > 0 ? (
+
+
setExpandedTables((p) => ({ ...p, [c.id]: !p[c.id] }))}
+ className="flex items-center gap-1 text-[.62rem] font-semibold px-1.5 py-0.5 rounded"
+ style={{ background: 'var(--bg)', color: 'var(--t3)' }}
+ >
+ {tables.length}
+ {isExpanded ? : }
+
+ {!isExpanded && tables.slice(0, 3).map((tb) => (
+
{tb.table_name}
+ ))}
+ {!isExpanded && tables.length > 3 && (
+
+{tables.length - 3}
+ )}
+
+ ) : (
+ {'\u2014'}
+ )}
+ {isExpanded && tables.length > 0 && (
+
+
+
+
+ {['Tabela', 'DescriΓ§Γ£o', 'Ativa', 'AΓ§Γ΅es'].map((h) => (
+ {h}
+ ))}
+
+
+
+ {tables.map((tb) => (
+
+
+ {tb.table_name}
+
+
+ {tb.description || 'β'}
+
+
+ handleToggleTable(c.id, tb.id, tb.is_active)} className="text-[.6rem] px-1.5 py-0.5 rounded font-medium" style={{ background: tb.is_active ? 'var(--gnl)' : 'var(--bg)', color: tb.is_active ? 'var(--gn)' : 'var(--t4)' }}>
+ {tb.is_active ? t('adb.yes') : t('adb.no')}
+
+
+
+ handleRemoveTable(c.id, tb.id)} className="text-[.58rem] px-1.5 py-0.5 rounded" style={{ color: 'var(--rd)', background: 'var(--rdl)' }}>
+ {t('adb.delete')}
+
+
+
+ ))}
+
+
+ {/* Add table inline */}
+ {editing?.id === c.id && (
+
+ )}
+
+ )}
+
+
+ {c.use_mtls ? (
+
+ ) : (
+ {'\u2014'}
+ )}
+
+
+ {c.wallet_dir ? (
+
+ ) : (
+
+ )}
+
+
+ {c.genai_config_id ? (
+
+ {emb?.name || c.embedding_model_id}
+
+ ) : (
+ {'\u2014'}
+ )}
+
+
+
+
+
startEdit(c)} className="btn btn-secondary btn-sm">
+ {t('adb.edit')}
+
+
handleTest(c.id)} disabled={testingMap[c.id]} className="btn btn-secondary btn-sm">
+ {testingMap[c.id] ? : } {t('adb.test')}
+
+ {confirmDelete === c.id ? (
+
+ handleDelete(c.id)} className="btn btn-sm" style={{ background: 'var(--rd)', color: '#fff' }}>
+ {t('adb.yes')}
+
+ setConfirmDelete(null)} className="btn btn-secondary btn-sm">
+ {t('adb.no')}
+
+
+ ) : (
+
setConfirmDelete(c.id)} className="btn btn-danger btn-sm">
+ {t('adb.delete')}
+
+ )}
+
+ {testResults[c.id] && (
+
+ {testResults[c.id].type === 's' ? : }
+ {testResults[c.id].text}
+
+ )}
+
+
+
+ );
+ })}
+
+
+
+ )}
+
+
+ {/* Form */}
+
+
{ if (!editing) setFormOpen(!formOpen); }}>
+
+
+ {editing ?
:
}
+
+ {editing ? `${t('adb.editConnection')} β ${editing.config_name}` : t('adb.newConnection')}
+
+
+
+
+ {isFormVisible && (
+
+
+ {editing
+ ? <>Editando: {editing.config_name} >
+ : <>{t('adb.connDesc')}>}
+
+
+ {formMsg &&
}
+
+ {/* Row 1 */}
+
+
+ {t('adb.connName')}
+ setCfgName(e.target.value)} placeholder="Producao ADB" className={inputCls} style={inputStyle} />
+
+
+
{t('adb.walletZip')}
+
+ {editing ? t('adb.walletEditHint') : t('adb.walletHint')}
+
+
+
+
+ {parsingWallet ? : }
+ {t('adb.analyze')}
+
+
+
+
+
+ {/* Row 2 */}
+
+
+
{t('adb.dsn')}
+
{t('adb.dsnHint')}
+ {dsnOptions.length > 0 ? (
+
setDsn(e.target.value)} className={inputCls} style={inputStyle}>
+ {dsnOptions.map((d) => (
+ {d}
+ ))}
+
+ ) : (
+
setDsn(e.target.value)} placeholder="myatp_high" className={inputCls} style={inputStyle} />
+ )}
+
+
+ {t('adb.username')}
+ setUsername(e.target.value)} placeholder="ADMIN" className={inputCls} style={inputStyle} />
+
+
+
+ {/* Row 3 */}
+
+
+ {/* Row 4 - Embedding config */}
+
+
+
{t('adb.genaiConfig')}
+
{t('adb.genaiConfigHint')}
+
setGenaiConfigId(e.target.value)} className={inputCls} style={inputStyle}>
+ {t('adb.noRag')}
+ {genaiCfg.map((g) => (
+ {g.name || g.model_id} {g.genai_region ? `- ${g.genai_region}` : ''}
+ ))}
+
+
+
+
{t('adb.embeddingModel')}
+
{t('adb.embeddingModelHint')}
+
setEmbeddingModelId(e.target.value)} className={inputCls} style={inputStyle}>
+ {Object.entries(embModels).map(([k, v]) => (
+ {v.name} ({v.dims}d)
+ ))}
+ {Object.keys(embModels).length === 0 && (
+ cohere.embed-v4.0
+ )}
+
+
+
+
+ {/* Buttons */}
+
+
+ {saving && }
+ {editing ? t('adb.saveChanges') : t('adb.saveConnection')}
+
+ {editing && (
+
+ {t('adb.cancel')}
+
+ )}
+
+
+ )}
+
+
+ {/* Wallet Upload card */}
+ {configs.length > 0 && (
+
+
+
+
+
+
{t('adb.updateWallet')}
+
+
+ {walletMsg &&
}
+
+
+ {t('adb.adbConnection')}
+ setWalletUploadId(e.target.value)} className={inputCls} style={inputStyle}>
+ {t('common.select')}
+ {configs.map((c) => (
+ {c.config_name}
+ ))}
+
+
+
+ {t('adb.walletZip')}
+
+
+
+
+
+ {uploadingWallet ? : }
+ {t('adb.uploadWallet')}
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/frontend-react/src/pages/config/EmbConsultPage.tsx b/frontend-react/src/pages/config/EmbConsultPage.tsx
new file mode 100644
index 0000000..04a794a
--- /dev/null
+++ b/frontend-react/src/pages/config/EmbConsultPage.tsx
@@ -0,0 +1,305 @@
+import { useState, useRef, useEffect, useCallback } from 'react';
+import { useAppStore, type AdbConfig, type AdbTable } from '@/stores/app';
+import { embeddingsApi, type ConsultResult } from '@/api/endpoints/embeddings';
+import { useI18n } from '@/i18n';
+import {
+ Search, Send, Loader2, Database, Trash2, User, Bot,
+ FileText, Hash,
+} from 'lucide-react';
+
+/* ββ Types ββ */
+interface ChatMessage {
+ role: 'user' | 'assistant';
+ content: string;
+ sources?: ConsultResult['documents'];
+ totalSources?: number;
+}
+
+/* ββ Markdown-lite renderer ββ */
+function renderMarkdown(text: string): string {
+ let html = text
+ .replace(/&/g, '&')
+ .replace(//g, '>');
+
+ // Bold
+ html = html.replace(/\*\*(.+?)\*\*/g, '$1 ');
+ // Italic
+ html = html.replace(/\*(.+?)\*/g, '$1 ');
+ // Inline code
+ html = html.replace(/`(.+?)`/g, '$1');
+ // Horizontal rule
+ html = html.replace(/\n---\n/g, ' ');
+ // Line breaks
+ html = html.replace(/\n/g, ' ');
+
+ return html;
+}
+
+/* ββ Main ββ */
+export default function EmbConsultPage() {
+ const { t } = useI18n();
+ const { adbCfg } = useAppStore();
+
+ // Config selectors
+ const [selTable, setSelTable] = useState('');
+ const [topK, setTopK] = useState(10);
+
+ // Chat
+ const [messages, setMessages] = useState([]);
+ const [query, setQuery] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const msgsRef = useRef(null);
+ const inputRef = useRef(null);
+
+ // Auto scroll
+ const scrollToBottom = useCallback(() => {
+ if (msgsRef.current) {
+ msgsRef.current.scrollTop = msgsRef.current.scrollHeight;
+ }
+ }, []);
+
+ useEffect(() => {
+ scrollToBottom();
+ }, [messages, loading, scrollToBottom]);
+
+ // Focus input on mount
+ useEffect(() => {
+ inputRef.current?.focus();
+ }, []);
+
+ // Table options from all active ADB configs
+ const allTables: { name: string; configName: string }[] = [];
+ for (const cfg of adbCfg) {
+ for (const t of (cfg.tables || []).filter((t) => t.is_active)) {
+ allTables.push({ name: t.table_name, configName: cfg.config_name });
+ }
+ }
+
+ // Preconditions
+ if (!adbCfg.length) {
+ return (
+
+
+
+
{t('emb.noAdb')}
+
{t('emb.noAdbHint')}
+
+
+ );
+ }
+
+ /* ββ Submit ββ */
+ const handleSubmit = async () => {
+ const q = query.trim();
+ if (!q || loading) return;
+
+ const userMsg: ChatMessage = { role: 'user', content: q };
+ setMessages((prev) => [...prev, userMsg]);
+ setQuery('');
+ setLoading(true);
+
+ try {
+ const d = await embeddingsApi.consult(q, selTable || undefined, topK);
+ let answer = d.answer || t('ec.noAnswer');
+
+ const assistantMsg: ChatMessage = {
+ role: 'assistant',
+ content: answer,
+ sources: d.documents,
+ totalSources: d.total,
+ };
+ setMessages((prev) => [...prev, assistantMsg]);
+ } catch (err) {
+ const assistantMsg: ChatMessage = {
+ role: 'assistant',
+ content: `Erro: ${err instanceof Error ? err.message : 'Erro desconhecido'}`,
+ };
+ setMessages((prev) => [...prev, assistantMsg]);
+ } finally {
+ setLoading(false);
+ inputRef.current?.focus();
+ }
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ handleSubmit();
+ }
+ };
+
+ const clearChat = () => {
+ setMessages([]);
+ inputRef.current?.focus();
+ };
+
+ return (
+
+
+
+ {/* Header */}
+
+
+
+
+
+
+
{t('ec.title')}
+
+ {t('ec.subtitle')}
+
+
+
+ {messages.length > 0 && (
+
+
+ {t('ec.clear')}
+
+ )}
+
+
+ {/* Config bar */}
+
+
+ {t('ec.tableOptional')}
+ setSelTable(e.target.value)}
+ className="w-full px-3 py-1.5 rounded-lg text-[.76rem] outline-none"
+ style={{ background: 'var(--bg1)', border: '1px solid var(--bd)', color: 'var(--t1)' }}
+ >
+ {t('ec.allTables')}
+ {allTables.map((t) => (
+ {t.name}
+ ))}
+
+
+
+ Top-K
+ setTopK(Math.max(1, Math.min(30, parseInt(e.target.value) || 10)))}
+ min={1}
+ max={30}
+ className="w-full px-3 py-1.5 rounded-lg text-[.76rem] outline-none text-center"
+ style={{ background: 'var(--bg1)', border: '1px solid var(--bd)', color: 'var(--t1)' }}
+ />
+
+
+
+ {/* Messages area */}
+
+ {messages.length === 0 && !loading && (
+
+
+
+
+ {t('ec.emptyTitle')}
+
+
+ {t('ec.emptyHint')}
+
+
+
+ )}
+
+ {messages.map((msg, i) => (
+
+ {msg.role === 'assistant' && (
+
+
+
+ )}
+
+ {msg.role === 'user' ? (
+
{msg.content}
+ ) : (
+
+ )}
+
+ {/* Sources */}
+ {msg.sources && msg.sources.length > 0 && (
+
+
+
+
+ {t('ec.sources')} ({msg.sources.length} documentos{msg.totalSources ? `, ${msg.totalSources} total` : ''})
+
+
+
+ {msg.sources.map((src, j) => (
+
+
+ {src.source}
+
+ dist: {src.distance.toFixed(4)}
+
+
+ ))}
+
+
+ )}
+
+ {msg.role === 'user' && (
+
+
+
+ )}
+
+ ))}
+
+ {/* Loading indicator */}
+ {loading && (
+
+
+
+
+
+
+ {t('ec.searching')}
+
+
+ )}
+
+
+ {/* Input bar */}
+
+ setQuery(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder={t('ec.placeholder')}
+ disabled={loading}
+ className="flex-1 px-4 py-2.5 rounded-xl text-[.82rem] outline-none transition-colors focus:ring-1 disabled:opacity-60"
+ style={{ background: 'var(--bg1)', border: '1.5px solid var(--bd)', color: 'var(--t1)' }}
+ />
+
+ {loading ? : }
+ {t('ec.submit')}
+
+
+
+
+
+ );
+}
diff --git a/frontend-react/src/pages/config/EmbeddingsPage.tsx b/frontend-react/src/pages/config/EmbeddingsPage.tsx
new file mode 100644
index 0000000..4605d2f
--- /dev/null
+++ b/frontend-react/src/pages/config/EmbeddingsPage.tsx
@@ -0,0 +1,505 @@
+import { useState, useRef, useCallback, useEffect } from 'react';
+import { useAppStore, type AdbConfig, type AdbTable } from '@/stores/app';
+import { embeddingsApi, type EmbeddingDoc } from '@/api/endpoints/embeddings';
+import { useI18n } from '@/i18n';
+import {
+ Dna, Shield, ScrollText, Upload, Loader2, Check, X,
+ Trash2, RefreshCw, Link, FileUp, Database, Search, AlertTriangle,
+} from 'lucide-react';
+
+/* ββ Helpers ββ */
+function Msg({ type, text }: { type: 's' | 'e' | 'i'; text: string }) {
+ const bg = type === 's' ? 'var(--gnl)' : type === 'e' ? 'var(--rdl)' : 'var(--bg2)';
+ const fg = type === 's' ? 'var(--gn)' : type === 'e' ? 'var(--rd)' : 'var(--t2)';
+ return (
+
+ {type === 's' && }
+ {type === 'e' && }
+ {type === 'i' && }
+ {text}
+
+ );
+}
+
+type MsgState = { type: 's' | 'e' | 'i'; text: string } | null;
+
+function getActiveTables(cfg: AdbConfig): AdbTable[] {
+ return (cfg.tables || []).filter((t) => t.is_active);
+}
+
+function getAllTables(cfg: AdbConfig): AdbTable[] {
+ return cfg.tables || [];
+}
+
+/* ββ Main ββ */
+export default function EmbeddingsPage() {
+ const { t } = useI18n();
+ const { adbCfg, ociCfg, genaiCfg } = useAppStore();
+
+ // ββ Existing Embeddings section ββ
+ const [selVid, setSelVid] = useState(() => adbCfg.find((c) => c.is_active)?.id || adbCfg[0]?.id || '');
+ const [selTable, setSelTable] = useState('');
+ const [docs, setDocs] = useState([]);
+ const [total, setTotal] = useState(0);
+ const [listLoading, setListLoading] = useState(false);
+ const [listMsg, setListMsg] = useState(null);
+
+ // ββ CIS Recommendations section ββ
+ const [cisVid, setCisVid] = useState(() => adbCfg.find((c) => c.is_active)?.id || adbCfg[0]?.id || '');
+ const cisPdfRef = useRef(null);
+ const [cisUploading, setCisUploading] = useState(false);
+ const [cisMsg, setCisMsg] = useState(null);
+
+ // ββ Knowledge Base section ββ
+ const [kbVid, setKbVid] = useState(() => adbCfg.find((c) => c.is_active)?.id || adbCfg[0]?.id || '');
+ const kbFileRef = useRef(null);
+ const [kbUrl, setKbUrl] = useState('');
+ const [kbFileUploading, setKbFileUploading] = useState(false);
+ const [kbUrlUploading, setKbUrlUploading] = useState(false);
+ const [kbMsg, setKbMsg] = useState(null);
+
+ // ββ Purge section ββ
+ const [purgeVid, setPurgeVid] = useState(() => adbCfg.find((c) => c.is_active)?.id || adbCfg[0]?.id || '');
+ const [purgeTable, setPurgeTable] = useState('');
+ const [purgeTenancy, setPurgeTenancy] = useState('');
+ const [purging, setPurging] = useState(false);
+ const [purgeMsg, setPurgeMsg] = useState(null);
+ const [purgeConfirm, setPurgeConfirm] = useState(false);
+
+ // Update selTable when selVid changes
+ useEffect(() => {
+ const cfg = adbCfg.find((c) => c.id === selVid);
+ if (cfg) {
+ const tables = getActiveTables(cfg);
+ setSelTable(tables[0]?.table_name || '');
+ }
+ }, [selVid, adbCfg]);
+
+ // Preconditions
+ if (!adbCfg.length) {
+ return (
+
+
+
+
{t('emb.noAdb')}
+
{t('emb.noAdbHint')}
+
+
+ );
+ }
+ if (!genaiCfg.length && !ociCfg.length) {
+ return (
+
+
+
+
{t('emb.noOci')}
+
{t('emb.noOciHint')}
+
+
+ );
+ }
+
+ /* ββ Handlers ββ */
+
+ const loadEmbs = async () => {
+ if (!selVid) return;
+ setListLoading(true);
+ setListMsg(null);
+ setDocs([]);
+ try {
+ const d = await embeddingsApi.list(selVid, selTable || undefined);
+ setDocs(d.documents);
+ setTotal(d.total);
+ if (!d.documents.length) setListMsg({ type: 'i', text: t('emb.noEmbeddings') });
+ } catch (err) {
+ setListMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro ao carregar embeddings' });
+ } finally {
+ setListLoading(false);
+ }
+ };
+
+ const deleteEmb = async (docId: string) => {
+ if (!confirm('Excluir este embedding?')) return;
+ try {
+ await embeddingsApi.remove(selVid, docId, selTable || undefined);
+ loadEmbs();
+ } catch (err) {
+ alert(err instanceof Error ? err.message : 'Erro ao excluir');
+ }
+ };
+
+ const uploadCis = async () => {
+ const f = cisPdfRef.current?.files?.[0];
+ if (!cisVid || !f) { setCisMsg({ type: 'e', text: t('emb.selectAdbAndFile') }); return; }
+ setCisUploading(true);
+ setCisMsg({ type: 'i', text: t('emb.processing') });
+ try {
+ const d = await embeddingsApi.uploadFile(cisVid, 'cisrecom', f);
+ setCisMsg({ type: 's', text: `${d.message} -> tabela CISRECOM` });
+ if (cisPdfRef.current) cisPdfRef.current.value = '';
+ } catch (err) {
+ setCisMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro' });
+ } finally {
+ setCisUploading(false);
+ }
+ };
+
+ const uploadKbFile = async () => {
+ const f = kbFileRef.current?.files?.[0];
+ if (!kbVid || !f) { setKbMsg({ type: 'e', text: t('emb.selectAdbAndFile') }); return; }
+ setKbFileUploading(true);
+ setKbMsg({ type: 'i', text: t('emb.uploading') });
+ try {
+ const d = await embeddingsApi.uploadFile(kbVid, 'engineerknowledgebase', f);
+ setKbMsg({ type: 's', text: d.message });
+ if (kbFileRef.current) kbFileRef.current.value = '';
+ } catch (err) {
+ setKbMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro' });
+ } finally {
+ setKbFileUploading(false);
+ }
+ };
+
+ const uploadKbUrl = async () => {
+ if (!kbVid || !kbUrl.trim()) { setKbMsg({ type: 'e', text: t('emb.selectAdbAndUrl') }); return; }
+ setKbUrlUploading(true);
+ setKbMsg({ type: 'i', text: t('emb.fetchingUrl') });
+ try {
+ const d = await embeddingsApi.uploadUrl(kbVid, 'engineerknowledgebase', kbUrl.trim());
+ setKbMsg({ type: 's', text: d.message });
+ setKbUrl('');
+ } catch (err) {
+ setKbMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro' });
+ } finally {
+ setKbUrlUploading(false);
+ }
+ };
+
+ const executePurge = async () => {
+ if (!purgeVid || !purgeTable) { setPurgeMsg({ type: 'e', text: t('emb.selectTable') }); return; }
+ setPurging(true);
+ setPurgeMsg({ type: 'i', text: t('emb.purging') });
+ try {
+ const d = await embeddingsApi.purge(purgeVid, purgeTable, purgeTenancy || undefined);
+ setPurgeMsg({ type: 's', text: `${d.deleted} embeddings removidos da tabela ${d.table} (tenancy: ${d.tenancy})` });
+ setPurgeConfirm(false);
+ } catch (err) {
+ setPurgeMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro' });
+ } finally {
+ setPurging(false);
+ }
+ };
+
+ /* ββ Shared styles ββ */
+ const inputCls = 'w-full px-3 py-2 rounded-lg text-[.78rem] outline-none transition-colors focus:ring-1';
+ const inputStyle = { background: 'var(--bg2)', border: '1.5px solid var(--bd)', color: 'var(--t1)' };
+ const labelCls = 'block text-[.72rem] font-semibold mb-1';
+ const labelStyle = { color: 'var(--t3)' };
+ const cardCls = 'card';
+ const cardStyle = { padding: 0 };
+ const cardHeaderCls = 'card-header';
+ const cardHeaderStyle = { margin: 0, padding: '14px 20px' };
+
+ const adbOptions = adbCfg.map((c) => (
+ {c.config_name}
+ ));
+
+ const tableOptions = (vid: string) => {
+ const cfg = adbCfg.find((c) => c.id === vid);
+ if (!cfg) return null;
+ return getActiveTables(cfg).map((t) => (
+ {t.table_name}
+ ));
+ };
+
+ const allTableOptions = (vid: string) => {
+ const cfg = adbCfg.find((c) => c.id === vid);
+ if (!cfg) return null;
+ return getAllTables(cfg).map((t) => (
+ {t.table_name}
+ ));
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
{t('emb.title')}
+
+ {t('emb.subtitle')}
+
+
+
+
+ {/* ββββββ Existing Embeddings ββββββ */}
+
+
+
+ {t('emb.existing')}
+
+
+
+ {t('emb.existingDesc')}
+
+
+
+ {t('emb.adbConnection')}
+ setSelVid(e.target.value)} className={inputCls} style={inputStyle}>
+ {adbOptions}
+
+
+
+ {t('emb.table')}
+ setSelTable(e.target.value)} className={inputCls} style={inputStyle}>
+ {tableOptions(selVid)}
+
+
+
+
+ {listLoading ? : }
+ {t('emb.load')}
+
+
+
+
+ {listMsg &&
}
+
+ {docs.length > 0 && (
+ <>
+
+
+
+
+ ID
+ Metadata
+
+
+
+
+ {docs.map((doc) => {
+ let meta = doc.metadata || '\u2014';
+ if (typeof meta === 'object') meta = JSON.stringify(meta);
+ if (meta.length > 200) meta = meta.substring(0, 200) + '\u2026';
+ return (
+
+
+
+ {doc.id.substring(0, 12)}
+
+
+
+
+ {meta}
+
+
+
+ deleteEmb(doc.id)} className="btn btn-sm" style={{ color: 'var(--rd)', background: 'none', border: 'none', padding: 4 }}>
+
+
+
+
+ );
+ })}
+
+
+
+
Total: {total} documentos
+ >
+ )}
+
+
+
+ {/* ββββββ CIS Recommendations ββββββ */}
+
+
+
+ {t('emb.cisTitle')}
+
+
+
+ {t('emb.cisDesc')}
+
+ {cisMsg &&
}
+
+
+ {t('emb.adbConnection')}
+ setCisVid(e.target.value)} className={inputCls} style={inputStyle}>
+ {adbOptions}
+
+
+
+ {t('emb.pdfFile')}
+
+
+
+
+ {cisUploading ? : }
+ {t('emb.uploadCis')}
+
+
+
+
+
+
+ {/* ββββββ Knowledge Base ββββββ */}
+
+
+
+ {t('emb.kbTitle')}
+
+
+
+ {t('emb.kbDesc')}
+
+ {kbMsg &&
}
+
+
+
+ {t('emb.adbConnection')}
+ setKbVid(e.target.value)} className={inputCls} style={inputStyle}>
+ {adbOptions}
+
+
+
+
+
+ {/* File upload */}
+
+ {t('emb.fileLabel')}
+
+
+ {kbFileUploading ? : }
+ {t('emb.uploadFile')}
+
+
+
+ {/* Divider */}
+
+
+ {/* URL import */}
+
+ {t('emb.urlLabel')}
+ setKbUrl(e.target.value)}
+ placeholder="https://docs.oracle.com/..."
+ className={inputCls}
+ style={inputStyle}
+ />
+
+ {kbUrlUploading ? : }
+ {t('emb.importUrl')}
+
+
+
+
+
+
+ {/* ββββββ Purge ββββββ */}
+
+
+
+
{t('emb.purgeTitle')}
+
+
+
+ {t('emb.purgeDesc')}
+
+ {purgeMsg &&
}
+
+
+ {t('emb.adbConnection')}
+ setPurgeVid(e.target.value)} className={inputCls} style={inputStyle}>
+ {adbOptions}
+
+
+
+ {t('emb.table')}
+ setPurgeTable(e.target.value)} className={inputCls} style={inputStyle}>
+ {t('common.select')}
+ {allTableOptions(purgeVid)}
+
+
+
+ {t('emb.tenancyOptional')}
+ setPurgeTenancy(e.target.value)}
+ placeholder="Filtrar por tenancy"
+ className={inputCls}
+ style={inputStyle}
+ />
+
+
+ {!purgeConfirm ? (
+
{ if (!purgeTable) { setPurgeMsg({ type: 'e', text: t('emb.selectTable') }); return; } setPurgeConfirm(true); }}
+ className="btn btn-danger btn-sm w-full justify-center"
+ >
+
+ {t('emb.purge')}
+
+ ) : (
+
+
+ {purging ? : }
+ {t('emb.confirm')}
+
+ setPurgeConfirm(false)}
+ className="px-3 py-2 rounded-lg text-[.72rem] font-medium"
+ style={{ background: 'var(--bg2)', color: 'var(--t3)', border: '1px solid var(--bd)' }}
+ >
+ {t('common.no')}
+
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/frontend-react/src/pages/config/GenAiConfigPage.tsx b/frontend-react/src/pages/config/GenAiConfigPage.tsx
new file mode 100644
index 0000000..1ff2290
--- /dev/null
+++ b/frontend-react/src/pages/config/GenAiConfigPage.tsx
@@ -0,0 +1,446 @@
+import { useState, useEffect, useCallback, useRef } from 'react';
+import { useAppStore } from '@/stores/app';
+import { useI18n } from '@/i18n';
+import { genaiApi, type GenAiConfigFull, type GenAiConfigPayload } from '@/api/endpoints/genai';
+import {
+ Brain, Plus, Pencil, Trash2, FlaskConical, Check, X, Loader2,
+ ChevronRight, Star, Link as LinkIcon,
+} from 'lucide-react';
+
+/* ββ Msg ββ */
+function Msg({ type, text }: { type: 's' | 'e' | 'i'; text: string }) {
+ const bg = type === 's' ? 'var(--gnl)' : type === 'e' ? 'var(--rdl)' : 'var(--bg2)';
+ const fg = type === 's' ? 'var(--gn)' : type === 'e' ? 'var(--rd)' : 'var(--t2)';
+ return (
+
+ {type === 's' && }
+ {type === 'e' && }
+ {type === 'i' && }
+
+
+ );
+}
+
+/* ββ Main ββ */
+export default function GenAiConfigPage() {
+ const { ociCfg, models, regions } = useAppStore();
+ const { t } = useI18n();
+
+ const [configs, setConfigs] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ // Form
+ const [formOpen, setFormOpen] = useState(false);
+ const [editing, setEditing] = useState(null);
+ const [saving, setSaving] = useState(false);
+ const [formMsg, setFormMsg] = useState<{ type: 's' | 'e' | 'i'; text: string } | null>(null);
+ const formRef = useRef(null);
+
+ // Form fields
+ const [name, setName] = useState('');
+ const [ociConfigId, setOciConfigId] = useState('');
+ const [modelId, setModelId] = useState('openai.gpt-4.1');
+ const [modelOcid, setModelOcid] = useState('');
+ const [compartmentId, setCompartmentId] = useState('');
+ const [genaiRegion, setGenaiRegion] = useState('us-ashburn-1');
+ const [isDefault, setIsDefault] = useState(false);
+
+ // Table
+ const [tableMsg, setTableMsg] = useState<{ type: 's' | 'e' | 'i'; text: string } | null>(null);
+ const [testResults, setTestResults] = useState>({});
+ const [testing, setTesting] = useState>({});
+ const [confirmDelete, setConfirmDelete] = useState(null);
+
+ const fetchConfigs = useCallback(async () => {
+ try {
+ const data = await genaiApi.list();
+ setConfigs(data);
+ } catch {} finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => { fetchConfigs(); }, [fetchConfigs]);
+
+ const resetForm = () => {
+ setEditing(null);
+ setFormOpen(false);
+ setName('');
+ setOciConfigId('');
+ setModelId('openai.gpt-4.1');
+ setModelOcid('');
+ setCompartmentId('');
+ setGenaiRegion('us-ashburn-1');
+ setIsDefault(false);
+ setFormMsg(null);
+ setSaving(false);
+ };
+
+ const startEdit = (cfg: GenAiConfigFull) => {
+ setEditing(cfg);
+ setName(cfg.name || '');
+ setOciConfigId(cfg.oci_config_id || '');
+ setModelId(cfg.model_ocid ? '_custom' : (cfg.model_id || 'openai.gpt-4.1'));
+ setModelOcid(cfg.model_ocid || '');
+ setCompartmentId(cfg.compartment_id || '');
+ setGenaiRegion(cfg.genai_region || 'us-ashburn-1');
+ setIsDefault(!!cfg.is_default);
+ setFormOpen(true);
+ setFormMsg(null);
+ setTimeout(() => formRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 100);
+ };
+
+ const fillFromOci = (ociId: string) => {
+ setOciConfigId(ociId);
+ const cfg = ociCfg.find((c) => c.id === ociId);
+ if (!cfg) return;
+ setGenaiRegion(cfg.region);
+ if (cfg.compartment_id) setCompartmentId(cfg.compartment_id);
+ setFormMsg({ type: 'i', text: `Regiao e Compartment preenchidos de ${cfg.tenancy_name} ` });
+ setTimeout(() => setFormMsg(null), 3000);
+ };
+
+ const handleSave = async () => {
+ if (!name.trim()) { setFormMsg({ type: 'e', text: t('genai.fillName') }); return; }
+ if (!ociConfigId) { setFormMsg({ type: 'e', text: t('genai.selectOci') }); return; }
+ if (modelId === '_custom' && !modelOcid) { setFormMsg({ type: 'e', text: t('genai.fillOcid') }); return; }
+
+ const body: GenAiConfigPayload = {
+ name: name.trim(),
+ oci_config_id: ociConfigId,
+ model_id: modelId === '_custom' ? 'custom' : modelId,
+ model_ocid: modelOcid || null,
+ compartment_id: compartmentId,
+ genai_region: genaiRegion,
+ endpoint: null,
+ serving_type: 'ON_DEMAND',
+ dedicated_endpoint_id: null,
+ is_default: editing ? !!editing.is_default : configs.length === 0,
+ };
+ if (isDefault) body.is_default = true;
+
+ setSaving(true);
+ setFormMsg({ type: 'i', text: t('genai.saving') });
+
+ try {
+ if (editing) {
+ await genaiApi.update(editing.id, body);
+ } else {
+ await genaiApi.create(body);
+ }
+ setFormMsg({ type: 's', text: `Modelo ${editing ? 'atualizado' : 'salvo'} com sucesso!` });
+ await fetchConfigs();
+ useAppStore.getState().loadData();
+ setTimeout(resetForm, 1200);
+ } catch (err) {
+ setFormMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro ao salvar' });
+ setSaving(false);
+ }
+ };
+
+ const handleDelete = async (id: string) => {
+ setConfirmDelete(null);
+ setTableMsg({ type: 'i', text: t('genai.deleting') });
+ try {
+ await genaiApi.remove(id);
+ await fetchConfigs();
+ useAppStore.getState().loadData();
+ setTableMsg({ type: 's', text: 'Modelo excluido com sucesso!' });
+ if (editing?.id === id) resetForm();
+ setTimeout(() => setTableMsg(null), 3000);
+ } catch (err) {
+ setTableMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro ao excluir' });
+ }
+ };
+
+ const handleTest = async (id: string) => {
+ setTesting((p) => ({ ...p, [id]: true }));
+ setTestResults((p) => { const n = { ...p }; delete n[id]; return n; });
+ try {
+ const res = await genaiApi.test(id);
+ setTestResults((p) => ({
+ ...p,
+ [id]: {
+ type: res.status === 'success' ? 's' : 'e',
+ text: res.message + (res.response ? ` β ${res.response}` : ''),
+ },
+ }));
+ } catch (err) {
+ setTestResults((p) => ({ ...p, [id]: { type: 'e', text: err instanceof Error ? err.message : 'Erro' } }));
+ } finally {
+ setTesting((p) => ({ ...p, [id]: false }));
+ }
+ };
+
+ // Group models by provider
+ const groupedModels = Object.entries(models).reduce>((acc, [key, m]) => {
+ const provider = m.provider || 'Other';
+ if (!acc[provider]) acc[provider] = [];
+ acc[provider].push({ key, name: m.name });
+ return acc;
+ }, {});
+
+ const isFormVisible = formOpen || !!editing;
+ const inputCls = "w-full px-3 py-2 rounded-lg text-[.78rem] outline-none transition-colors focus:ring-1";
+ const inputStyle = { background: 'var(--bg2)', border: '1.5px solid var(--bd)', color: 'var(--t1)' };
+ const labelCls = "block";
+ const labelStyle = {};
+ const hintCls = "text-[.64rem] mt-0.5";
+ const hintStyle = { color: 'var(--t4)' };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
+
{t('genai.title')}
+
{t('genai.subtitle')}
+
+
+
+ {configs.length} {configs.length === 1 ? t('genai.model') : t('genai.models')}
+
+
+
+ {/* Table */}
+
+
+
+
+
+
{t('genai.configured')}
+
+
+ {tableMsg &&
}
+
+ {loading ? (
+
+
+
+ ) : configs.length === 0 ? (
+
+
+
{t('genai.noModels')}
+
+ ) : (
+
+
+
+
+ {['Config', 'Modelo', 'OCI Config', 'Regiao', 'Acoes'].map((h) => (
+ {h}
+ ))}
+
+
+
+ {configs.map((c) => {
+ const mi = models[c.model_id];
+ const oci = ociCfg.find((o) => o.id === c.oci_config_id);
+ return (
+ { if (editing?.id !== c.id) e.currentTarget.style.background = 'var(--bg2)'; }}
+ onMouseLeave={(e) => { if (editing?.id !== c.id) e.currentTarget.style.background = ''; }}
+ >
+
+
+ {c.name || '\u2014'}
+ {c.is_default && (
+
+ {t('genai.default')}
+
+ )}
+
+
+
+
+ {mi?.name || (c.model_ocid ? 'OCID Personalizado' : c.model_id)}
+
+
+ {mi?.provider || (c.model_ocid ? 'custom' : '?')}
+
+
+
+
+ {oci?.tenancy_name || '\u2014'}
+
+
+
+
+ {c.genai_region}
+
+
+
+
+
+
startEdit(c)} className="btn btn-secondary btn-sm">
+ {t('oci.edit')}
+
+
handleTest(c.id)} disabled={testing[c.id]} className="btn btn-secondary btn-sm">
+ {testing[c.id] ? : } {t('oci.test')}
+
+ {confirmDelete === c.id ? (
+
+ handleDelete(c.id)} className="btn btn-sm" style={{ background: 'var(--rd)', color: '#fff' }}>
+ {t('common.yes')}
+
+ setConfirmDelete(null)} className="btn btn-secondary btn-sm">
+ {t('common.no')}
+
+
+ ) : (
+
setConfirmDelete(c.id)} className="btn btn-danger btn-sm">
+ {t('oci.delete')}
+
+ )}
+
+ {testResults[c.id] && (
+
+ {testResults[c.id].type === 's' ? : }
+ {testResults[c.id].text}
+
+ )}
+
+
+
+ );
+ })}
+
+
+
+ )}
+
+
+ {/* Form */}
+
+
{ if (!editing) setFormOpen(!formOpen); }}
+ >
+
+
+ {editing ?
:
}
+
+ {editing ? `${t('genai.editModel')} β ${editing.name}` : t('genai.newModel')}
+
+
+
+
+ {isFormVisible && (
+
+
+ {editing
+ ? <>Editando: {editing.name} >
+ : t('genai.addDesc')}
+
+
+ {formMsg &&
}
+
+ {/* Row 1 */}
+
+
+ {t('genai.configName')}
+ setName(e.target.value)} placeholder="Meu Modelo Custom" className={inputCls} style={inputStyle} />
+
+
+ {t('genai.ociCredential')}
+ fillFromOci(e.target.value)} className={inputCls} style={inputStyle}>
+ {t('common.select')}
+ {ociCfg.map((c) => (
+ {c.tenancy_name} ({c.region})
+ ))}
+
+
+
+ {t('genai.modelSelect')}
+ setModelId(e.target.value)} className={inputCls} style={inputStyle}>
+ {t('genai.customOcid')}
+ {Object.entries(groupedModels).map(([provider, items]) => (
+
+ {items.map((m) => (
+ {m.name} ({provider})
+ ))}
+
+ ))}
+
+
+
+
+ {/* Row 2 */}
+
+
+ {t('genai.genaiRegion')}
+ setGenaiRegion(e.target.value)} className={inputCls} style={inputStyle}>
+ {regions.map((r) => (
+ {r}
+ ))}
+
+
+
+ {t('genai.compartment')}
+ setCompartmentId(e.target.value)} placeholder="ocid1.compartment.oc1.." className={inputCls} style={inputStyle} />
+
+
+
{t('genai.modelOcid')}
+
{t('genai.modelOcidHint')}
+
setModelOcid(e.target.value)}
+ placeholder="ocid1.generativeaimodel.oc1.iad.amaaaaaa..."
+ className={inputCls}
+ style={{ ...inputStyle, borderColor: modelOcid ? 'var(--pp)' : undefined, fontFamily: 'var(--fm)', fontSize: '.72rem' }}
+ />
+
+
+
+ {/* Default toggle */}
+
+ setIsDefault(!isDefault)}
+ className="flex items-center gap-2 px-3 py-1.5 rounded-lg text-[.72rem] font-semibold transition-all"
+ style={{
+ background: isDefault ? 'color-mix(in srgb, #f39c12 15%, transparent)' : 'var(--bg2)',
+ color: isDefault ? '#f39c12' : 'var(--t4)',
+ border: `1.5px solid ${isDefault ? '#f39c12' : 'var(--bd)'}`,
+ }}
+ >
+ {isDefault ? t('genai.default') : t('genai.setDefault')}
+
+
+
+ {/* Buttons */}
+
+
+ {saving && }
+ {editing ? t('genai.saveChanges') : t('genai.saveModel')}
+
+ {editing && (
+
+ {t('genai.cancel')}
+
+ )}
+
+
+ )}
+
+
+
+ );
+}
diff --git a/frontend-react/src/pages/config/McpServersPage.tsx b/frontend-react/src/pages/config/McpServersPage.tsx
new file mode 100644
index 0000000..dd57c80
--- /dev/null
+++ b/frontend-react/src/pages/config/McpServersPage.tsx
@@ -0,0 +1,523 @@
+import { useState, useEffect, useCallback, useRef } from 'react';
+import { useAppStore } from '@/stores/app';
+import { useI18n } from '@/i18n';
+import { mcpApi, type McpServerFull, type McpServerPayload, type McpToolDef } from '@/api/endpoints/mcp';
+import {
+ Plug, Plus, Pencil, Trash2, Check, X, Loader2, ChevronRight,
+ Power, PowerOff, Search, Wrench, Terminal, Globe, FileCode2, ChevronDown, ChevronUp,
+} from 'lucide-react';
+
+/* ββ Msg ββ */
+function Msg({ type, text }: { type: 's' | 'e' | 'i'; text: string }) {
+ const bg = type === 's' ? 'var(--gnl)' : type === 'e' ? 'var(--rdl)' : 'var(--bg2)';
+ const fg = type === 's' ? 'var(--gn)' : type === 'e' ? 'var(--rd)' : 'var(--t2)';
+ return (
+
+ {type === 's' && }
+ {type === 'e' && }
+ {type === 'i' && }
+ {text}
+
+ );
+}
+
+/* ββ Type badge ββ */
+function TypeBadge({ type }: { type: string }) {
+ const cfg: Record = {
+ stdio: { icon: , bg: 'color-mix(in srgb, var(--ac) 12%, transparent)', fg: 'var(--ac)' },
+ sse: { icon: , bg: 'color-mix(in srgb, var(--gn) 12%, transparent)', fg: 'var(--gn)' },
+ module: { icon: , bg: 'color-mix(in srgb, #e67e22 12%, transparent)', fg: '#e67e22' },
+ };
+ const c = cfg[type] || cfg.stdio;
+ return (
+
+ {c.icon} {type}
+
+ );
+}
+
+/* ββ Main ββ */
+export default function McpServersPage() {
+ const { adbCfg } = useAppStore();
+ const { t } = useI18n();
+
+ const [servers, setServers] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ // Form
+ const [formOpen, setFormOpen] = useState(false);
+ const [editing, setEditing] = useState(null);
+ const [saving, setSaving] = useState(false);
+ const [formMsg, setFormMsg] = useState<{ type: 's' | 'e' | 'i'; text: string } | null>(null);
+ const formRef = useRef(null);
+
+ // Form fields
+ const [sName, setSName] = useState('');
+ const [sDesc, setSDesc] = useState('');
+ const [sType, setSType] = useState<'stdio' | 'sse' | 'module'>('stdio');
+ const [sCmd, setSCmd] = useState('');
+ const [sArgs, setSArgs] = useState('');
+ const [sUrl, setSUrl] = useState('');
+ const [sAdb, setSAdb] = useState('');
+
+ // Table
+ const [tableMsg, setTableMsg] = useState<{ type: 's' | 'e' | 'i'; text: string } | null>(null);
+ const [confirmDelete, setConfirmDelete] = useState(null);
+ const [toggling, setToggling] = useState>({});
+ const [discovering, setDiscovering] = useState>({});
+ const [expandedTools, setExpandedTools] = useState>({});
+
+ const fetchServers = useCallback(async () => {
+ try {
+ const data = await mcpApi.list();
+ setServers(data);
+ } catch {} finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => { fetchServers(); }, [fetchServers]);
+
+ const resetForm = () => {
+ setEditing(null);
+ setFormOpen(false);
+ setSName('');
+ setSDesc('');
+ setSType('stdio');
+ setSCmd('');
+ setSArgs('');
+ setSUrl('');
+ setSAdb('');
+ setFormMsg(null);
+ setSaving(false);
+ };
+
+ const startEdit = (srv: McpServerFull) => {
+ setEditing(srv);
+ setSName(srv.name);
+ setSDesc(srv.description || '');
+ setSType((srv.server_type || 'stdio') as 'stdio' | 'sse' | 'module');
+ setSCmd(srv.command || '');
+ setSArgs(srv.args ? JSON.stringify(srv.args) : '');
+ setSUrl(srv.url || '');
+ setSAdb(srv.linked_adb_id || '');
+ setFormOpen(true);
+ setFormMsg(null);
+ setTimeout(() => formRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 100);
+ };
+
+ const handleSave = async () => {
+ if (!sName.trim()) { setFormMsg({ type: 'e', text: t('mcp.fillName') }); return; }
+
+ let args: string[] | null = null;
+ if (sArgs.trim()) {
+ try { args = JSON.parse(sArgs); } catch { setFormMsg({ type: 'e', text: t('mcp.invalidArgs') }); return; }
+ }
+
+ const body: McpServerPayload = {
+ name: sName.trim(),
+ description: sDesc.trim() || null,
+ server_type: sType,
+ command: sType !== 'sse' ? (sCmd.trim() || null) : null,
+ url: sType === 'sse' ? (sUrl.trim() || null) : null,
+ args,
+ linked_adb_id: sAdb || null,
+ };
+
+ setSaving(true);
+ setFormMsg({ type: 'i', text: editing ? t('mcp.updating') : t('mcp.saving') });
+
+ try {
+ if (editing) {
+ await mcpApi.update(editing.id, body);
+ } else {
+ await mcpApi.create(body);
+ }
+ setFormMsg({ type: 's', text: `Server ${editing ? 'atualizado' : 'registrado'} com sucesso!` });
+ await fetchServers();
+ useAppStore.getState().loadData();
+ setTimeout(resetForm, 1200);
+ } catch (err) {
+ setFormMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro ao salvar' });
+ setSaving(false);
+ }
+ };
+
+ const handleDelete = async (id: string) => {
+ setConfirmDelete(null);
+ setTableMsg({ type: 'i', text: 'Excluindo server...' });
+ try {
+ await mcpApi.remove(id);
+ await fetchServers();
+ useAppStore.getState().loadData();
+ setTableMsg({ type: 's', text: 'Server excluido com sucesso!' });
+ if (editing?.id === id) resetForm();
+ setTimeout(() => setTableMsg(null), 3000);
+ } catch (err) {
+ setTableMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro ao excluir' });
+ }
+ };
+
+ const handleToggle = async (id: string) => {
+ setToggling((p) => ({ ...p, [id]: true }));
+ try {
+ await mcpApi.toggle(id);
+ await fetchServers();
+ useAppStore.getState().loadData();
+ } catch (err) {
+ setTableMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro' });
+ } finally {
+ setToggling((p) => ({ ...p, [id]: false }));
+ }
+ };
+
+ const handleDiscover = async (id: string) => {
+ setDiscovering((p) => ({ ...p, [id]: true }));
+ setTableMsg({ type: 'i', text: t('mcp.discovering') });
+ try {
+ const res = await mcpApi.discoverTools(id);
+ await fetchServers();
+ useAppStore.getState().loadData();
+ setTableMsg({ type: 's', text: `${res.discovered} tool(s) descoberta(s), ${res.total} total` });
+ setExpandedTools((p) => ({ ...p, [id]: true }));
+ setTimeout(() => setTableMsg(null), 4000);
+ } catch (err) {
+ setTableMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro ao descobrir tools' });
+ } finally {
+ setDiscovering((p) => ({ ...p, [id]: false }));
+ }
+ };
+
+ const handleRemoveTool = async (srvId: string, idx: number) => {
+ const srv = servers.find((s) => s.id === srvId);
+ if (!srv?.tools) return;
+ const tool = srv.tools[idx];
+ const toolName = typeof tool === 'string' ? tool : (tool as McpToolDef).name;
+ if (!confirm(`Remover tool "${toolName}"?`)) return;
+ const newTools = [...srv.tools];
+ newTools.splice(idx, 1);
+ try {
+ await mcpApi.updateTools(srvId, newTools);
+ await fetchServers();
+ useAppStore.getState().loadData();
+ } catch (err) {
+ setTableMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro' });
+ }
+ };
+
+ const isFormVisible = formOpen || !!editing;
+ const inputCls = "w-full px-3 py-2 rounded-lg text-[.78rem] outline-none transition-colors focus:ring-1";
+ const inputStyle = { background: 'var(--bg2)', border: '1.5px solid var(--bd)', color: 'var(--t1)' };
+ const labelCls = "block";
+ const labelStyle = {};
+ const hintCls = "text-[.64rem] mt-0.5";
+ const hintStyle = { color: 'var(--t4)' };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
{t('mcp.title')}
+
{t('mcp.subtitle')}
+
+
+
+ {servers.length} {servers.length === 1 ? 'server' : 'servers'}
+
+
+
+ {/* Server cards */}
+
+
+
+
{t('mcp.registered')}
+
+
+ {tableMsg &&
}
+
+ {loading ? (
+
+
+
+ ) : servers.length === 0 ? (
+
+
+
{t('mcp.noServers')}
+
+ ) : (
+
+ {servers.map((srv) => {
+ const tools: McpToolDef[] = Array.isArray(srv.tools) ? srv.tools : [];
+ const isExpanded = expandedTools[srv.id];
+ const adb = srv.linked_adb_id ? adbCfg.find((a) => a.id === srv.linked_adb_id) : null;
+ return (
+
+ {/* Header row */}
+
+
+
{srv.name}
+
+
+ {srv.is_active ? : }
+ {srv.is_active ? t('mcp.active') : t('mcp.inactive')}
+
+
+
+
startEdit(srv)} className="btn btn-secondary btn-sm">
+ {t('mcp.edit')}
+
+
handleToggle(srv.id)}
+ disabled={toggling[srv.id]}
+ className="btn btn-secondary btn-sm"
+ >
+ {toggling[srv.id] ? : srv.is_active ? : }
+ {srv.is_active ? t('mcp.deactivate') : t('mcp.activate')}
+
+ {confirmDelete === srv.id ? (
+
+ handleDelete(srv.id)} className="btn btn-sm" style={{ background: 'var(--rd)', color: '#fff' }}>
+ {t('common.yes')}
+
+ setConfirmDelete(null)} className="btn btn-secondary btn-sm">
+ {t('common.no')}
+
+
+ ) : (
+
setConfirmDelete(srv.id)} className="btn btn-danger btn-sm">
+ {t('mcp.delete')}
+
+ )}
+
+
+
+ {/* Description */}
+ {srv.description && (
+
{srv.description}
+ )}
+
+ {/* Details row */}
+
+ {srv.command && (
+
+ {t('mcp.command')} {' '}
+ {srv.command}
+
+ )}
+ {srv.url && (
+
+ {t('mcp.url')} {' '}
+ {srv.url}
+
+ )}
+ {srv.module_path && (
+
+ {t('mcp.module')} {' '}
+ {srv.module_path}
+
+ )}
+
+ {t('mcp.adb')} {' '}
+ {adb ? (
+ {adb.config_name}
+ ) : t('mcp.noAdb')}
+
+
+
+ {/* Tools section */}
+
+
setExpandedTools((p) => ({ ...p, [srv.id]: !p[srv.id] }))}
+ >
+
+
+ {t('mcp.tools')} ({tools.length})
+
+ {isExpanded ? : }
+
+
+ {isExpanded && tools.length > 0 && (
+
+ {tools.map((tool, i) => (
+
+
+
{typeof tool === 'string' ? tool : tool.name}
+ {typeof tool === 'object' && tool.description && (
+
β {tool.description}
+ )}
+ {typeof tool === 'object' && tool.input_schema && (tool.input_schema as { properties?: Record
}).properties && (
+
+ Params: {Object.keys((tool.input_schema as { properties: Record }).properties).join(', ')}
+
+ )}
+
+
handleRemoveTool(srv.id, i)}
+ className="flex-shrink-0 px-1.5 py-0.5 rounded text-[.6rem] font-semibold transition-colors"
+ style={{ color: 'var(--rd)', background: 'color-mix(in srgb, var(--rd) 8%, transparent)' }}
+ >
+
+
+
+ ))}
+
+ )}
+ {isExpanded && tools.length === 0 && (
+
{t('mcp.noTools')}
+ )}
+
+
+ {/* Action buttons */}
+
+ handleDiscover(srv.id)}
+ disabled={discovering[srv.id]}
+ className="btn btn-success btn-sm"
+ >
+ {discovering[srv.id] ? : }
+ {t('mcp.discoverTools')}
+
+
+
+ );
+ })}
+
+ )}
+
+
+ {/* Form */}
+
+
{ if (!editing) setFormOpen(!formOpen); }}>
+
+
+ {editing ?
:
}
+
+ {editing ? `${t('mcp.editServer')} β ${editing.name}` : t('mcp.newServer')}
+
+
+
+
+ {isFormVisible && (
+
+
+ {editing
+ ? <>Editando: {editing.name} >
+ : t('mcp.configDesc')}
+
+
+ {formMsg &&
}
+
+ {/* Name + description */}
+
+
+ {/* Server type */}
+
+
{t('mcp.serverType')}
+
+ {(['stdio', 'sse', 'module'] as const).map((tp) => {
+ const icons = { stdio: , sse: , module: };
+ const labels = { stdio: 'stdio', sse: 'SSE (HTTP)', module: 'Python Module' };
+ const selected = sType === tp;
+ return (
+ setSType(tp)}
+ className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[.72rem] font-semibold transition-all"
+ style={{
+ background: selected ? 'color-mix(in srgb, var(--gn) 15%, transparent)' : 'var(--bg2)',
+ color: selected ? 'var(--gn)' : 'var(--t4)',
+ border: `1.5px solid ${selected ? 'var(--gn)' : 'var(--bd)'}`,
+ }}
+ >
+ {icons[tp]} {labels[tp]}
+
+ );
+ })}
+
+
+
+ {/* Conditional fields */}
+ {sType !== 'sse' && (
+
+
+
{t('mcp.commandLabel')}
+
{t('mcp.commandHint')}
+
setSCmd(e.target.value)} placeholder="python3 server.py" className={inputCls} style={inputStyle} />
+
+
+
{t('mcp.argsLabel')}
+
{t('mcp.argsHint')}
+
setSArgs(e.target.value)} placeholder='["--config", "/path/to/config"]' className={inputCls} style={{ ...inputStyle, fontFamily: 'var(--fm)', fontSize: '.72rem' }} />
+
+
+ )}
+ {sType === 'sse' && (
+
+
{t('mcp.urlLabel')}
+
{t('mcp.urlHint')}
+
setSUrl(e.target.value)} placeholder="http://localhost:8001/sse" className={inputCls} style={inputStyle} />
+
+ )}
+
+ {/* ADB link */}
+
+
{t('mcp.linkAdb')}
+
{t('mcp.linkAdbHint')}
+
setSAdb(e.target.value)} className={inputCls} style={inputStyle}>
+ {t('mcp.noAdb')}
+ {adbCfg.map((a) => (
+ {a.config_name}
+ ))}
+
+
+
+ {/* Buttons */}
+
+
+ {saving && }
+ {editing ? t('mcp.saveChanges') : t('mcp.saveServer')}
+
+ {editing && (
+
+ {t('mcp.cancel')}
+
+ )}
+
+
+ )}
+
+
+
+ );
+}
diff --git a/frontend-react/src/pages/config/OciConfigPage.tsx b/frontend-react/src/pages/config/OciConfigPage.tsx
new file mode 100644
index 0000000..5cda31f
--- /dev/null
+++ b/frontend-react/src/pages/config/OciConfigPage.tsx
@@ -0,0 +1,775 @@
+import { useState, useEffect, useRef, useCallback } from 'react';
+import { useAppStore } from '@/stores/app';
+import { useI18n } from '@/i18n';
+import { ociApi, type OciConfigFull, type OciTestResult } from '@/api/endpoints/oci';
+import {
+ Cloud, ChevronRight, Key, Lock, Plus, Pencil,
+ Trash2, FlaskConical, Building2, User, Package,
+ Check, X, Loader2, Search,
+} from 'lucide-react';
+
+/* ββ Static fallback OCI regions ββ */
+const FALLBACK_REGIONS: Record = {
+ 'Americas': [
+ 'us-ashburn-1', 'us-phoenix-1', 'us-sanjose-1', 'us-chicago-1',
+ 'ca-toronto-1', 'ca-montreal-1', 'sa-saopaulo-1', 'sa-vinhedo-1',
+ 'sa-santiago-1', 'sa-bogota-1', 'mx-queretaro-1', 'mx-monterrey-1',
+ ],
+ 'Europe': [
+ 'eu-frankfurt-1', 'eu-amsterdam-1', 'eu-zurich-1', 'eu-madrid-1',
+ 'eu-marseille-1', 'eu-milan-1', 'eu-stockholm-1', 'eu-paris-1',
+ 'uk-london-1', 'uk-cardiff-1', 'eu-jovanovac-1', 'eu-dcc-milan-1',
+ ],
+ 'Asia Pacific': [
+ 'ap-tokyo-1', 'ap-osaka-1', 'ap-seoul-1', 'ap-sydney-1',
+ 'ap-melbourne-1', 'ap-mumbai-1', 'ap-hyderabad-1', 'ap-singapore-1',
+ 'ap-chuncheon-1', 'ap-singapore-2',
+ ],
+ 'Middle East & Africa': [
+ 'me-jeddah-1', 'me-dubai-1', 'me-abudhabi-1',
+ 'af-johannesburg-1', 'il-jerusalem-1',
+ ],
+};
+
+type AuthType = 'api_key' | 'session_token';
+
+/* ββ Msg component ββ */
+function Msg({ type, text }: { type: 's' | 'e' | 'i'; text: string }) {
+ const bg = type === 's' ? 'var(--gnl)' : type === 'e' ? 'var(--rdl)' : 'var(--bg2)';
+ const fg = type === 's' ? 'var(--gn)' : type === 'e' ? 'var(--rd)' : 'var(--t2)';
+ return (
+
+ {type === 's' && }
+ {type === 'e' && }
+ {type === 'i' && }
+ {text}
+
+ );
+}
+
+/* ββ Region Dropdown ββ */
+function RegionDropdown({
+ value, onChange, regionsMap, t,
+}: { value: string; onChange: (r: string) => void; regionsMap: Record; t: (key: string) => string }) {
+ const [open, setOpen] = useState(false);
+ const [filter, setFilter] = useState('');
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const handler = (e: MouseEvent) => {
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
+ };
+ document.addEventListener('mousedown', handler);
+ return () => document.removeEventListener('mousedown', handler);
+ }, []);
+
+ const f = filter.toLowerCase();
+
+ return (
+
+
setOpen(!open)}
+ className="flex items-center justify-between cursor-pointer px-3 py-2 rounded-lg text-[.78rem]"
+ style={{ background: 'var(--bg2)', border: '1.5px solid var(--bd)', minHeight: 36 }}
+ >
+
+ {value || t('oci.selectRegion')}
+
+ ▼
+
+
+ {open && (
+
+
+
+
+ setFilter(e.target.value)}
+ onClick={(e) => e.stopPropagation()}
+ className="w-full border-none outline-none text-xs py-1.5"
+ style={{ background: 'transparent', color: 'var(--t1)' }}
+ autoFocus
+ />
+
+
+
+ {Object.entries(regionsMap).map(([grp, regs]) => {
+ const filtered = regs.filter((r) => !f || r.includes(f));
+ if (!filtered.length) return null;
+ return (
+
+
+ {grp}
+
+ {filtered.map((r) => (
+
{ onChange(r); setOpen(false); setFilter(''); }}
+ className="px-3 py-1.5 text-xs cursor-pointer transition-colors"
+ style={{
+ color: value === r ? 'var(--ac)' : 'var(--t2)',
+ background: value === r ? 'color-mix(in srgb, var(--ac) 8%, transparent)' : undefined,
+ }}
+ onMouseEnter={(e) => { if (value !== r) (e.target as HTMLDivElement).style.background = 'var(--bg2)'; }}
+ onMouseLeave={(e) => { if (value !== r) (e.target as HTMLDivElement).style.background = ''; }}
+ >
+ {r}
+
+ ))}
+
+ );
+ })}
+ {Object.entries(regionsMap).every(([, regs]) => !regs.some((r) => !f || r.includes(f))) && (
+
{t('oci.noRegion')}
+ )}
+
+
+ )}
+
+ );
+}
+
+/* ββ Auth Type Tag ββ */
+function AuthTag({ type }: { type: string }) {
+ const isToken = type === 'session_token';
+ return (
+
+ {isToken ? : }
+ {isToken ? 'Session Token' : 'API Key'}
+
+ );
+}
+
+/* ββ Main Page ββ */
+export default function OciConfigPage() {
+ const ociRegions = useAppStore((s) => s.ociRegions);
+ const { t } = useI18n();
+
+ const [configs, setConfigs] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ // Form state
+ const [formOpen, setFormOpen] = useState(false);
+ const [editing, setEditing] = useState(null);
+ const [authType, setAuthType] = useState('api_key');
+ const [region, setRegion] = useState('');
+ const [saving, setSaving] = useState(false);
+ const [formMsg, setFormMsg] = useState<{ type: 's' | 'e' | 'i'; text: string } | null>(null);
+
+ // Table msg
+ const [tableMsg, setTableMsg] = useState<{ type: 's' | 'e' | 'i'; text: string } | null>(null);
+
+ // Test results per config
+ const [testResults, setTestResults] = useState>({});
+ const [testing, setTesting] = useState>({});
+
+ // Delete confirm
+ const [confirmDelete, setConfirmDelete] = useState(null);
+
+ // Form refs
+ const formRef = useRef(null);
+ const tnRef = useRef(null);
+ const toRef = useRef(null);
+ const uoRef = useRef(null);
+ const fpRef = useRef(null);
+ const cpRef = useRef(null);
+ const kpRef = useRef(null);
+ const skRef = useRef(null);
+ const pkRef = useRef(null);
+ const stkRef = useRef(null);
+ const sshRef = useRef(null);
+
+ // Build regions map
+ const regionsMap: Record = (() => {
+ if (ociRegions && typeof ociRegions === 'object') {
+ const entries = Object.entries(ociRegions);
+ if (entries.length > 0) {
+ // ociRegions can be { group: string[] } or { group: string }
+ // From the store it's Record, but the backend returns Record
+ const first = entries[0][1];
+ if (Array.isArray(first)) return ociRegions as unknown as Record;
+ // If it's flat, return as single-group
+ const flat = entries.map(([, v]) => v as unknown as string);
+ if (flat.length > 0) return { 'Regioes': flat };
+ }
+ }
+ return FALLBACK_REGIONS;
+ })();
+
+ const fetchConfigs = useCallback(async () => {
+ try {
+ const data = await ociApi.list();
+ setConfigs(data);
+ } catch {
+ // silent
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => { fetchConfigs(); }, [fetchConfigs]);
+
+ const resetForm = () => {
+ setEditing(null);
+ setFormOpen(false);
+ setAuthType('api_key');
+ setRegion('');
+ setFormMsg(null);
+ setSaving(false);
+ // Clear file inputs
+ if (skRef.current) skRef.current.value = '';
+ if (pkRef.current) pkRef.current.value = '';
+ };
+
+ const startEdit = (cfg: OciConfigFull) => {
+ setEditing(cfg);
+ setAuthType(cfg.auth_type || 'api_key');
+ setRegion(cfg.region);
+ setFormOpen(true);
+ setFormMsg(null);
+ setTimeout(() => formRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 100);
+ };
+
+ const handleSave = async () => {
+ const tenancyName = tnRef.current?.value?.trim() || '';
+ if (!tenancyName) { setFormMsg({ type: 'e', text: `${t('oci.fillField')} Tenancy Name` }); return; }
+
+ const isEdit = !!editing;
+ const at = isEdit ? (editing!.auth_type || 'api_key') : authType;
+ const isToken = at === 'session_token';
+
+ const tenancyOcid = toRef.current?.value?.trim() || '';
+ const userOcid = uoRef.current?.value?.trim() || '';
+ const fingerprint = fpRef.current?.value?.trim() || '';
+ const compartmentId = cpRef.current?.value?.trim() || '';
+ const keyPassphrase = kpRef.current?.value?.trim() || '';
+ const securityToken = stkRef.current?.value?.trim() || '';
+ const sshPubKey = sshRef.current?.value?.trim() || '';
+ const privateKeyFile = skRef.current?.files?.[0];
+ const publicKeyFile = pkRef.current?.files?.[0];
+
+ // Validations for new config
+ if (!isEdit) {
+ if (!tenancyOcid) { setFormMsg({ type: 'e', text: t('oci.fillOcidTenancy') }); return; }
+ if (!isToken && (!userOcid || !fingerprint)) { setFormMsg({ type: 'e', text: t('oci.fillUserAndFP') }); return; }
+ if (isToken && !securityToken) { setFormMsg({ type: 'e', text: t('oci.pasteToken') }); return; }
+ if (!privateKeyFile) { setFormMsg({ type: 'e', text: isToken ? t('oci.selectSessionKey') : t('oci.selectKey') }); return; }
+ }
+
+ const fd = new FormData();
+ fd.append('tenancy_name', tenancyName);
+ fd.append('tenancy_ocid', tenancyOcid);
+ fd.append('user_ocid', userOcid);
+ fd.append('fingerprint', fingerprint);
+ fd.append('region', region);
+ fd.append('compartment_id', compartmentId);
+ fd.append('key_passphrase', keyPassphrase);
+ fd.append('ssh_public_key', sshPubKey);
+ fd.append('auth_type', at);
+ fd.append('security_token', securityToken);
+ if (privateKeyFile) fd.append('private_key', privateKeyFile);
+ if (publicKeyFile) fd.append('public_key', publicKeyFile);
+
+ setSaving(true);
+ setFormMsg({ type: 'i', text: t('oci.saving') });
+
+ try {
+ if (isEdit) {
+ await ociApi.update(editing!.id, fd);
+ } else {
+ await ociApi.create(fd);
+ }
+ setFormMsg({ type: 's', text: isEdit ? t('oci.updated') : t('oci.saved') });
+ await fetchConfigs();
+ useAppStore.getState().loadData();
+ setTimeout(resetForm, 1200);
+ } catch (err) {
+ setFormMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro ao salvar' });
+ setSaving(false);
+ }
+ };
+
+ const handleDelete = async (id: string) => {
+ setConfirmDelete(null);
+ setTableMsg({ type: 'i', text: t('oci.deleting') });
+ try {
+ await ociApi.remove(id);
+ await fetchConfigs();
+ useAppStore.getState().loadData();
+ setTableMsg({ type: 's', text: t('oci.deleted') });
+ if (editing?.id === id) resetForm();
+ setTimeout(() => setTableMsg(null), 3000);
+ } catch (err) {
+ setTableMsg({ type: 'e', text: err instanceof Error ? err.message : 'Erro ao excluir' });
+ }
+ };
+
+ const handleTest = async (id: string) => {
+ setTesting((p) => ({ ...p, [id]: true }));
+ setTestResults((p) => { const n = { ...p }; delete n[id]; return n; });
+ try {
+ const res = await ociApi.test(id);
+ setTestResults((p) => ({ ...p, [id]: res }));
+ } catch (err) {
+ setTestResults((p) => ({ ...p, [id]: { status: 'error', message: err instanceof Error ? err.message : 'Erro' } }));
+ } finally {
+ setTesting((p) => ({ ...p, [id]: false }));
+ }
+ };
+
+ const isFormVisible = formOpen || !!editing;
+ const currentAuthType = editing ? (editing.auth_type || 'api_key') : authType;
+ const isToken = currentAuthType === 'session_token';
+
+ /* ββ Input helper ββ */
+ const inputCls = "w-full px-3 py-2 rounded-lg text-[.78rem] outline-none transition-colors focus:ring-1";
+ const inputStyle = { background: 'var(--bg2)', border: '1.5px solid var(--bd)', color: 'var(--t1)' };
+ const labelCls = "block";
+ const labelStyle = {};
+ const hintCls = "text-[.64rem] mt-0.5";
+ const hintStyle = { color: 'var(--t4)' };
+
+ return (
+
+
+ {/* ββ Header ββ */}
+
+
+
+
+
+
{t('oci.title')}
+
{t('oci.subtitle')}
+
+
+
+ {configs.length} {configs.length === 1 ? t('oci.credential') : t('oci.credentials')}
+
+
+
+ {/* ββ Table ββ */}
+
+
+
+
+
+
{t('oci.registered')}
+
+
+ {tableMsg &&
{ }
}
+
+ {loading ? (
+
+
+
+ ) : configs.length === 0 ? (
+
+
+
{t('oci.noCredentials')}
+
+ ) : (
+
+
+
+
+ {['Tenancy', 'Tipo', 'Region', 'Detalhes', 'Acoes'].map((h) => (
+ {h}
+ ))}
+
+
+
+ {configs.map((c) => (
+ { if (editing?.id !== c.id) (e.currentTarget.style.background = 'var(--bg2)'); }}
+ onMouseLeave={(e) => { if (editing?.id !== c.id) (e.currentTarget.style.background = ''); }}
+ >
+ {/* Tenancy */}
+
+ {c.tenancy_name}
+ {c.created_at}
+
+ {/* Auth Type */}
+
+ {/* Region */}
+
+
+ {c.region}
+
+
+ {/* Details */}
+
+
+ {(c.auth_type || 'api_key') === 'session_token' ? (
+ <>
+
{c.tenancy_ocid}
+
Session Token
+ >
+ ) : (
+ <>
+
{'*'.repeat(20)}
+
{c.tenancy_ocid}
+
{c.user_ocid}
+ >
+ )}
+
{c.compartment_id || '\u2014'}
+
SSH: {c.ssh_public_key_preview || {t('oci.sshNotConfigured')} }
+
+
+ {/* Actions */}
+
+
+
+
startEdit(c)}
+ className="btn btn-secondary btn-sm"
+ >
+