diff --git a/backend/app.py b/backend/app.py index f8e2af8..81077ce 100644 --- a/backend/app.py +++ b/backend/app.py @@ -323,6 +323,12 @@ def init_db(): action TEXT NOT NULL, resource TEXT, details TEXT, ip TEXT, created_at TEXT DEFAULT (datetime('now')) ); + CREATE TABLE IF NOT EXISTS terminal_history ( + id TEXT PRIMARY KEY, user_id TEXT NOT NULL, + oci_config_id TEXT NOT NULL, command TEXT NOT NULL, + exit_code INTEGER, output TEXT, error TEXT, + created_at TEXT DEFAULT (datetime('now')) + ); CREATE TABLE IF NOT EXISTS config_logs ( id TEXT PRIMARY KEY, config_type TEXT NOT NULL, @@ -1089,6 +1095,145 @@ async def test_oci(cid: str, u=Depends(require("admin","user"))): _config_log("oci", cid, cname, "error", "test", str(e)[:500], u["id"], u["username"]) return {"status":"error","message":str(e)[:500]} +# ── OCI CLI Terminal ───────────────────────────────────────────────────────── +_TERMINAL_BLOCKED_PATTERNS = re.compile( + r'(rm\s|mv\s|cp\s|chmod\s|chown\s|sudo\s|bash\s|sh\s|curl\s|wget\s|python|pip\s|apt\s|yum\s|>\s|>>\s|\|)', + re.IGNORECASE +) + +@app.post("/api/terminal/execute") +async def terminal_execute(req: dict, u=Depends(current_user)): + """Execute an OCI CLI command using the selected OCI config. Only 'oci' commands allowed.""" + oci_config_id = req.get("oci_config_id", "") + command = (req.get("command", "") or "").strip() + if not oci_config_id: + raise HTTPException(400, "oci_config_id obrigatório") + if not command: + raise HTTPException(400, "Comando vazio") + _verify_config_access("oci", oci_config_id, u) + # Security: only allow 'oci' commands + if not command.startswith("oci "): + raise HTTPException(400, "Apenas comandos 'oci' são permitidos. Ex: oci iam user list") + if _TERMINAL_BLOCKED_PATTERNS.search(command): + raise HTTPException(400, "Comando contém operações não permitidas") + # Build config path + cfg_path = OCI_DIR / oci_config_id / "config" + if not cfg_path.exists(): + raise HTTPException(400, "OCI config file não encontrado. Reconfigure as credenciais.") + # Execute with timeout + import shlex, subprocess + cmd_args = shlex.split(command) + ["--config-file", str(cfg_path)] + try: + loop = asyncio.get_event_loop() + result = await asyncio.wait_for( + loop.run_in_executor(_chat_executor, lambda: subprocess.run( + cmd_args, capture_output=True, text=True, timeout=60, cwd="/tmp", + env={**os.environ, "OCI_CLI_SUPPRESS_FILE_PERMISSIONS_WARNING": "True", "HOME": "/tmp"} + )), timeout=65) + output = result.stdout or "" + error = result.stderr or "" + exit_code = result.returncode + # Save to history + with db() as c: + c.execute("INSERT INTO terminal_history (id,user_id,oci_config_id,command,exit_code,output,error) VALUES (?,?,?,?,?,?,?)", + (str(uuid.uuid4()), u["id"], oci_config_id, command, exit_code, output[:10000], error[:2000])) + return { + "exit_code": exit_code, + "output": output[:50000], + "error": error[:5000] if exit_code != 0 else "", + } + except asyncio.TimeoutError: + with db() as c: + c.execute("INSERT INTO terminal_history (id,user_id,oci_config_id,command,exit_code,error) VALUES (?,?,?,?,?,?)", + (str(uuid.uuid4()), u["id"], oci_config_id, command, -1, "Timeout: 60s")) + return {"exit_code": -1, "output": "", "error": "Timeout: comando excedeu 60 segundos"} + except Exception as e: + return {"exit_code": -1, "output": "", "error": str(e)[:2000]} + +@app.get("/api/terminal/history") +async def terminal_history(oci_config_id: str = Query(""), limit: int = Query(50), u=Depends(current_user)): + """Get recent terminal commands for the user.""" + with db() as c: + if oci_config_id: + rows = c.execute( + "SELECT * FROM terminal_history WHERE user_id=? AND oci_config_id=? ORDER BY created_at DESC LIMIT ?", + (u["id"], oci_config_id, limit)).fetchall() + else: + rows = c.execute( + "SELECT * FROM terminal_history WHERE user_id=? ORDER BY created_at DESC LIMIT ?", + (u["id"], limit)).fetchall() + return [dict(r) for r in rows] + +# OCI CLI autocomplete cache +_oci_completions_cache: dict = {} + +def _parse_oci_help(args: list[str]) -> list[str]: + """Run oci --help and extract available commands/options.""" + import subprocess + try: + result = subprocess.run( + ["oci"] + args + ["--help"], capture_output=True, text=True, timeout=10, + env={**os.environ, "OCI_CLI_SUPPRESS_FILE_PERMISSIONS_WARNING": "True", "HOME": "/tmp"}) + text = result.stdout + result.stderr + commands = [] + in_commands = False + for line in text.splitlines(): + stripped = line.strip() + # Detect transition into command/service listing + if not in_commands: + if re.match(r'^(Commands|Options|Core|Others|Networking|Database|Identity|Storage|Compute|Security)', stripped): + in_commands = True + continue + if not stripped: + continue + # Section headers (single word or capitalized phrase not indented) — skip + if not line.startswith(" "): + continue + # Capture " command-name Description text" pattern + m = re.match(r'^ ([a-z][a-z0-9\-_]+)\s', line) + if m and len(m.group(1)) > 1: + commands.append(m.group(1)) + continue + # Capture 2-space indent commands (sub-command help) + m = re.match(r'^ ([a-z][a-z0-9\-_]+)\s', line) + if m and len(m.group(1)) > 2: + commands.append(m.group(1)) + continue + # Capture --options + if stripped.startswith("--"): + opt = stripped.split()[0].rstrip(",") + if opt.startswith("--"): commands.append(opt) + return sorted(set(commands)) + except Exception: + return [] + +@app.get("/api/terminal/completions") +async def terminal_completions(prefix: str = Query(""), u=Depends(current_user)): + """Get OCI CLI autocomplete suggestions for a partial command.""" + parts = prefix.strip().split() + if not parts or parts[0] != "oci": + return {"suggestions": []} + # Build cache key from command path (without final partial word) + # e.g. "oci iam us" → lookup completions for ["oci", "iam"], filter by "us" + if prefix.endswith(" "): + cmd_parts = parts[1:] + filter_prefix = "" + else: + cmd_parts = parts[1:-1] + filter_prefix = parts[-1] if len(parts) > 1 else "" + cache_key = " ".join(cmd_parts) + if cache_key not in _oci_completions_cache: + loop = asyncio.get_event_loop() + completions = await loop.run_in_executor(_chat_executor, lambda: _parse_oci_help(cmd_parts)) + _oci_completions_cache[cache_key] = completions + suggestions = _oci_completions_cache[cache_key] + if filter_prefix: + suggestions = [s for s in suggestions if s.startswith(filter_prefix)] + # Commands first, then options + cmds = [s for s in suggestions if not s.startswith("--")] + opts = [s for s in suggestions if s.startswith("--")] + return {"suggestions": (cmds + opts)[:50]} + # ── OCI Account Explorer ────────────────────────────────────────────────────── @app.get("/api/oci/explore/{cid}/compartments") async def explore_compartments(cid: str, u=Depends(current_user)): diff --git a/frontend-react/src/App.tsx b/frontend-react/src/App.tsx index 42d1e77..4471cee 100644 --- a/frontend-react/src/App.tsx +++ b/frontend-react/src/App.tsx @@ -20,6 +20,7 @@ const McpServersPage = lazy(() => import('@/pages/config/McpServersPage')); const AdbConfigPage = lazy(() => import('@/pages/config/AdbConfigPage')); const EmbeddingsPage = lazy(() => import('@/pages/config/EmbeddingsPage')); const EmbConsultPage = lazy(() => import('@/pages/config/EmbConsultPage')); +const TerminalPage = lazy(() => import('@/pages/TerminalPage')); const UsersPage = lazy(() => import('@/pages/admin/UsersPage')); const MySettingsPage = lazy(() => import('@/pages/admin/MySettingsPage')); const OracleIamPage = lazy(() => import('@/pages/admin/OracleIamPage')); @@ -61,6 +62,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend-react/src/components/layout/Sidebar.tsx b/frontend-react/src/components/layout/Sidebar.tsx index 9d8af17..9ebb126 100644 --- a/frontend-react/src/components/layout/Sidebar.tsx +++ b/frontend-react/src/components/layout/Sidebar.tsx @@ -1,9 +1,9 @@ import { NavLink } from 'react-router-dom'; import { useState } from 'react'; import { - MessageSquare, Search, BarChart3, Cloud, Shield, + MessageSquare, Search, BarChart3, Cloud, Shield, Terminal, Brain, Plug, Database, Dna, Users, FileText, - Sparkles, Sun, Moon, LogOut, FolderOpen, Languages, Settings, ArrowLeft, + Sparkles, Sun, Moon, LogOut, FolderOpen, Settings, ArrowLeft, UserCog, ChevronDown, ChevronRight, } from 'lucide-react'; import { useTheme } from '@/hooks/useTheme'; @@ -52,6 +52,7 @@ const NAV_ITEMS: NavItem[] = [ { to: '/oci-services', icon: , labelKey: 'nav.ociServices' }, { to: '/reports', icon: , labelKey: 'nav.reports' }, { to: '/config/embeddings/consult', icon: , labelKey: 'nav.embConsult' }, + { to: '/terminal', icon: , labelKey: 'nav.terminal' }, ]; const ADMIN_ITEMS: NavItem[] = [ @@ -73,7 +74,7 @@ const USER_MGMT_ITEMS: NavItem[] = [ export default function Sidebar() { const { toggle, isDark } = useTheme(); const { user, logout } = useAuthStore(); - const { t, lang, setLang } = useI18n(); + const { t } = useI18n(); const [showSettings, setShowSettings] = useState(false); const [userMgmtOpen, setUserMgmtOpen] = useState(false); @@ -196,18 +197,6 @@ export default function Sidebar() { > {isDark ? : } -
diff --git a/frontend-react/src/i18n/en.ts b/frontend-react/src/i18n/en.ts index b80b7df..b332b49 100644 --- a/frontend-react/src/i18n/en.ts +++ b/frontend-react/src/i18n/en.ts @@ -20,6 +20,7 @@ const en: Record = { 'nav.adb': 'ADB Vector', 'nav.embeddings': 'Embeddings', 'nav.embConsult': 'Query Embeddings', + 'nav.terminal': 'Terminal', 'nav.userMgmt': 'User Management', 'nav.users': 'Users', 'nav.mySettings': 'My Settings', @@ -755,6 +756,8 @@ const en: Record = { 'settings.timezone': 'Timezone', 'settings.tzDesc': 'Set your timezone for date and time display.', 'settings.tzChanged': 'Timezone changed successfully', + 'settings.language': 'Language', + 'settings.langDesc': 'Select the interface language.', 'settings.changePassword': 'Change Password', 'settings.currentPw': 'Current Password', 'settings.newPw': 'New Password', @@ -767,6 +770,16 @@ const en: Record = { 'settings.pwMinLength': 'New password must be at least 8 characters', 'settings.federatedInfo': 'Password and MFA are managed by Oracle IAM Identity Domain. Configure directly in the OCI portal.', 'settings.mfa': 'Multi-Factor Authentication (MFA)', + // ── Terminal ── + 'term.title': 'OCI CLI Terminal', + 'term.subtitle': 'Execute OCI CLI commands directly in the browser', + 'term.config': 'Configuration', + 'term.selectConfig': 'Select an OCI configuration', + 'term.welcome': 'Type OCI CLI commands. Ex: oci iam user list --compartment-id ', + 'term.clearHint': 'to clear', + 'term.historyHint': 'to browse history', + 'term.executing': 'Executing...', + 'term.noOutput': 'Command executed with no output', // ── Oracle IAM ── 'iam.title': 'Oracle IAM Identity Domains', 'iam.subtitle': 'OIDC integration for corporate authentication', diff --git a/frontend-react/src/i18n/es.ts b/frontend-react/src/i18n/es.ts new file mode 100644 index 0000000..cd8d1e7 --- /dev/null +++ b/frontend-react/src/i18n/es.ts @@ -0,0 +1,811 @@ +const es: Record = { + // ── Sidebar ── + 'nav.principal': 'Principal', + 'nav.admin': 'Administracion', + 'nav.chat': 'Chat Agent', + 'nav.terraform': 'Terraform', + 'nav.promptGen': 'Prompt Generator', + 'nav.workspaces': 'Workspaces', + 'nav.explorer': 'OCI Explorer', + 'nav.ociServices': 'OCI Services', + 'svc.title': 'Estado de Servicios OCI', + 'svc.subtitle': 'Verifique y configure el estado de los servicios de seguridad OCI por tenencia', + 'svc.redetect': 'Re-detectar', + 'svc.checking': 'Verificando servicios OCI...', + 'svc.save': 'Guardar', + 'nav.reports': 'Reportes', + 'nav.ociCreds': 'Credenciales OCI', + 'nav.genai': 'Configuracion GenAI', + 'nav.mcp': 'Servidores MCP', + 'nav.adb': 'ADB Vector', + 'nav.embeddings': 'Embeddings', + 'nav.embConsult': 'Consultar Embeddings', + 'nav.terminal': 'Terminal', + 'nav.userMgmt': 'Gestión de Usuarios', + 'nav.users': 'Usuarios', + 'nav.mySettings': 'Mi Configuracion', + 'nav.oracleIam': 'Oracle IAM', + 'nav.audit': 'Registro de Auditoria', + 'sidebar.lightMode': 'Modo claro', + 'sidebar.darkMode': 'Modo oscuro', + 'sidebar.logout': 'Cerrar sesion', + 'sidebar.settings': 'Configuracion', + 'sidebar.backToMenu': 'Volver al menu', + 'sidebar.roleAdmin': 'Administrador', + 'sidebar.roleUser': 'Usuario', + 'sidebar.subtitle': 'Infraestructura y Seguridad', + + // ── Login ── + 'login.subtitle': 'Ingeniero de Infraestructura y Seguridad', + 'login.userPlaceholder': 'Usuario', + 'login.passPlaceholder': 'Contrasena', + 'login.mfaScanQr': 'Escanee el codigo QR con su aplicacion de autenticacion:', + 'login.totpPlaceholder': 'Codigo TOTP', + 'login.loading': 'Iniciando sesion...', + 'login.verify': 'Verificar', + 'login.submit': 'Iniciar sesion', + 'login.error': 'Error de inicio de sesion', + + // ── Chat ── + 'chat.timeNow': 'ahora', + 'chat.today': 'Hoy', + 'chat.yesterday': 'Ayer', + 'chat.last7': 'Ultimos 7 dias', + 'chat.last30': 'Ultimos 30 dias', + 'chat.older': 'Anteriores', + 'chat.other': 'Otros', + 'chat.history': 'Historial', + 'chat.newChat': 'Nuevo Chat', + 'chat.untitled': 'Sin titulo', + 'chat.noConversations': 'Aun no hay conversaciones', + 'chat.selectModel': 'Seleccione un modelo...', + 'chat.savedConfigs': 'Configuraciones Guardadas', + 'chat.searchModel': 'Buscar modelo...', + 'chat.noModels': 'No se encontraron modelos', + 'chat.ociCred': 'Credencial OCI...', + 'chat.settings': 'Configuracion', + 'chat.newChatTitle': 'Nuevo chat', + 'chat.emptyTitle': 'Inicie una conversacion con el agente.', + 'chat.emptySubtitle': 'Seleccione un modelo para comenzar.', + 'chat.thinking': 'Pensando...', + 'chat.retry': 'Reintentar', + 'chat.attachFile': 'Adjuntar archivo', + 'chat.placeholder': 'Escriba su mensaje... (Shift+Enter para nueva linea)', + 'chat.send': 'Enviar', + 'chat.selectOciCred': 'Seleccione una credencial OCI para usar modelos directos.', + 'chat.sendError': 'Error al enviar mensaje', + 'chat.requestFailed': 'La solicitud fallo.', + 'chat.timeout': 'Tiempo agotado: la respuesta esta tardando demasiado. Intente de nuevo.', + 'chat.toolsAvailable': 'herramientas disponibles', + 'chat.ociCredLabel': 'Credencial OCI', + 'chat.select': 'Seleccione...', + 'chat.analyzeFiles': 'Analizar los archivos adjuntos.', + + // ── Terraform ── + 'tf.history': 'Historial', + 'tf.newChat': 'Nuevo', + 'tf.noConversations': 'Sin conversaciones', + 'tf.searchModel': 'Buscar modelo...', + 'tf.selectModel': 'Seleccione modelo...', + 'tf.savedConfigs': 'Configuraciones Guardadas', + 'tf.ociConfig': 'Configuracion OCI...', + 'tf.settings': 'Configuracion', + 'tf.newConversation': 'Nueva conversacion', + 'tf.emptyTitle': 'Describa la infraestructura OCI', + 'tf.emptySubtitle': 'El agente generara codigo Terraform basado en su descripcion.', + 'tf.thinking': 'Procesando...', + 'tf.retry': 'Reintentar', + 'tf.placeholder': 'Describa la infraestructura deseada... (Shift+Enter para nueva linea)', + 'tf.send': 'Enviar', + 'tf.workspace': 'Workspace', + 'tf.files': 'Archivos', + 'tf.terminal': 'Terminal', + 'tf.resources': 'Recursos', + 'tf.plan': 'Plan', + 'tf.apply': 'Apply', + 'tf.destroy': 'Destroy', + 'tf.cancel': 'Cancelar', + 'tf.download': 'Descargar ZIP', + 'tf.createWs': 'Crear Workspace', + 'tf.noFiles': 'No se generaron archivos', + 'tf.timeout': 'Tiempo agotado: la generacion esta tardando demasiado.', + 'tf.selectOci': 'Seleccione una credencial OCI', + 'tf.compartment': 'Compartimiento', + 'tf.region': 'Region', + 'tf.rollback': 'Rollback', + 'tf.copy': 'Copiar', + 'tf.copied': 'Copiado', + 'tf.noWs': 'Sin workspace activo', + 'tf.deleteWs': 'Eliminar', + 'tf.noFilesToPlan': 'Sin archivos para planificar', + 'tf.selectOciConfig': 'Seleccione Configuracion OCI y Compartimiento', + 'tf.running': 'Ejecutando...', + 'tf.terminalTitle': 'Terminal de salida Terraform', + 'tf.terminalHint': 'Ejecute Plan, Apply o Destroy para ver los resultados aqui', + 'tf.autoFix': 'Auto-corregir', + 'tf.editSystemPrompt': 'Editar System Prompt', + 'tf.refreshReference': 'Actualizar Referencia', + + // ── Prompt Generator ── + 'pg.history': 'Historial', + 'pg.new': 'Nuevo', + 'pg.noConversations': 'Sin conversaciones', + 'pg.emptyTitle': 'Describa la infraestructura OCI que necesita', + 'pg.emptySubtitle': 'El agente generara un prompt estructurado y optimizado para el Terraform Agent.', + 'pg.loading': 'Generando prompt...', + 'pg.placeholder': 'Describa la infraestructura deseada... (Shift+Enter para nueva linea)', + 'pg.copy': 'Copiar', + 'pg.copied': 'Copiado', + 'pg.retry': 'Reintentar', + 'pg.timeout': 'Tiempo agotado: la generacion esta tardando demasiado.', + 'pg.search': 'Buscar...', + 'pg.selectModel': 'Seleccione modelo...', + + // ── Workspaces ── + 'ws.title': 'Terraform Workspaces', + 'ws.all': 'Todos', + 'ws.active': 'Activo', + 'ws.draft': 'Borrador', + 'ws.failed': 'Fallido', + 'ws.destroyed': 'Destruido', + 'ws.loading': 'Cargando...', + 'ws.refresh': 'Actualizar', + 'ws.noWs': 'No se encontraron workspaces', + 'ws.noWsHint': 'Cree workspaces a traves del Terraform Agent', + 'ws.loadingWs': 'Cargando workspaces...', + 'ws.open': 'Abrir', + 'ws.plan': 'Plan', + 'ws.apply': 'Apply', + 'ws.destroy': 'Destroy', + 'ws.delete': 'Eliminar', + 'ws.cancel': 'Cancelar', + 'ws.hideOutput': 'ocultar', + 'ws.viewOutput': 'ver salida', + 'ws.confirmDestroy': 'Confirmar Destruccion', + 'ws.confirmDestroyMsg': 'Esta accion destruira todos los recursos aprovisionados por este workspace.', + 'ws.typeDestroy': 'Escriba DESTROY para confirmar:', + 'ws.deleteWs': 'Eliminar Workspace', + 'ws.deleteWsMsg': 'Eliminar este workspace permanentemente? Los archivos locales seran removidos.', + 'ws.noDate': 'Sin fecha', + + // ── Explorer ── + 'exp.title': 'OCI Explorer', + 'exp.subtitle': 'Explore los recursos de su Oracle Cloud Infrastructure', + 'exp.search': 'Buscar recurso...', + 'exp.refresh': 'Actualizar', + 'exp.loading': 'Cargando...', + 'exp.noResources': 'No se encontraron recursos', + 'exp.selectResource': 'Seleccione un tipo de recurso', + 'exp.compartment': 'Compartimiento', + 'exp.region': 'Region', + 'exp.allRegions': 'Todas las regiones', + 'exp.start': 'Iniciar', + 'exp.stop': 'Detener', + 'exp.details': 'Detalles', + 'exp.total': 'Total', + 'exp.network': 'Red', + 'exp.observability': 'Observabilidad', + 'exp.security': 'Seguridad', + 'exp.selectRegions': 'Seleccione regiones', + 'exp.nRegions': '{0} regiones', + 'exp.all': 'Todas', + 'exp.none': 'Ninguna', + 'exp.noCompartment': 'No se encontraron compartimientos', + 'exp.selectConfig': 'Seleccione una configuracion OCI', + 'exp.loadingResources': 'Cargando recursos...', + 'exp.clearFilter': 'Intente limpiar el filtro de busqueda', + 'exp.resources': 'recursos', + 'exp.updating': 'actualizando...', + 'exp.regions': 'region(es)', + 'exp.filter': 'Filtrar...', + 'exp.startAction': 'Iniciar', + 'exp.stopAction': 'Detener', + 'exp.updatingState': 'Actualizando...', + 'exp.createdAt': 'Creado el', + 'exp.public': 'Publico', + 'exp.yesPublic': 'Si', + 'exp.noPrivate': 'No (privado)', + 'exp.freeTier': 'Free Tier', + 'exp.yes': 'Si', + 'exp.no': 'No', + 'exp.noRestrictions': 'Sin restricciones (abierto)', + 'exp.updateNetworkAccess': 'Actualizar Acceso de Red', + 'exp.accessUpdated': 'Acceso actualizado: ', + 'exp.objects': 'Objetos', + 'exp.size': 'Tamano', + 'exp.visibility': 'Visibilidad', + 'exp.private': 'Privado', + 'exp.rules': 'Reglas', + 'exp.routes': 'Rutas', + 'exp.hideRules': 'Ocultar reglas', + 'exp.viewRules': 'Ver reglas', + 'exp.add': 'Agregar', + 'exp.direction': 'Direccion', + 'exp.protocol': 'Protocolo', + 'exp.originDest': 'Origen/Destino', + 'exp.ports': 'Puertos', + 'exp.description': 'Descripcion', + 'exp.portMin': 'Puerto Min', + 'exp.portMax': 'Puerto Max', + 'exp.myIp': 'Mi IP', + 'exp.save': 'Guardar', + 'exp.cancel': 'Cancelar', + 'exp.ruleAdded': 'Regla agregada.', + 'exp.enterCidr': 'Ingrese el CIDR.', + 'exp.removeRule': 'Eliminar esta regla?', + 'exp.removeNsgRule': 'Eliminar esta regla NSG?', + 'exp.ipDetected': 'IP detectada: {0} — ajuste el puerto y haga clic en Guardar', + 'exp.ipDetectFailed': 'No se pudo detectar la IP.', + 'exp.destination': 'Destino', + 'exp.destType': 'Tipo Destino', + 'exp.version': 'Version', + 'exp.ha': 'HA', + + // ── Reports ── + 'rpt.title': 'Reportes CIS', + 'rpt.subtitle': 'Ejecute escaneos de cumplimiento CIS OCI Benchmark', + 'rpt.startScan': 'Iniciar Escaneo', + 'rpt.running': 'Ejecutando...', + 'rpt.cancel': 'Cancelar', + 'rpt.tenancy': 'Tenencia', + 'rpt.level': 'Nivel', + 'rpt.region': 'Region', + 'rpt.allRegions': 'Todas las regiones', + 'rpt.noReports': 'No se encontraron reportes', + 'rpt.noReportsHint': 'Ejecute un escaneo para generar reportes de cumplimiento.', + 'rpt.progress': 'Progreso', + 'rpt.completed': 'Completado', + 'rpt.failed': 'Fallido', + 'rpt.viewReport': 'Ver Reporte', + 'rpt.download': 'Descargar', + 'rpt.delete': 'Eliminar', + 'rpt.summary': 'Resumen', + 'rpt.passed': 'Aprobado', + 'rpt.findings': 'Hallazgos', + 'rpt.recommendations': 'Recomendaciones', + 'rpt.selectTenancy': 'Seleccione una tenencia', + 'rpt.selectCredential': 'Seleccione una credencial', + 'rpt.loadingReports': 'Cargando reportes...', + 'rpt.refresh': 'Actualizar', + 'rpt.pageTitle': 'Reportes CIS', + 'rpt.pageSubtitle': 'CIS Benchmark 3.0 — Escaneo de Cumplimiento y Dashboard', + 'rpt.runScanCis': 'Ejecutar Escaneo CIS', + 'rpt.inExecution': 'En ejecucion', + 'rpt.configParams': 'Configure los parametros y ejecute el escaneo de cumplimiento CIS.', + 'rpt.ociCredential': 'Credencial OCI', + 'rpt.selectOciCred': 'Seleccione credencial OCI...', + 'rpt.cisLevel': 'Nivel CIS', + 'rpt.level1Desc': 'Verificaciones esenciales de seguridad', + 'rpt.level2Desc': 'Verificaciones completas (incluye Nivel 1)', + 'rpt.regions': 'Regiones', + 'rpt.leaveEmpty': 'Deje vacio para escanear todas las regiones suscritas', + 'rpt.executeCompliance': 'Ejecutar Cumplimiento CIS', + 'rpt.scanProgress': 'Progreso del Escaneo', + 'rpt.hideLog': 'Ocultar log', + 'rpt.showLog': 'Ver log completo', + 'rpt.lines': 'lineas', + 'rpt.cancelScan': 'Cancelar Escaneo', + 'rpt.selectReport': 'Seleccionar Reporte', + 'rpt.selectCompletedReport': 'Seleccione un reporte completado...', + 'rpt.searchTenancy': 'Buscar tenencia...', + 'rpt.searchByTenancy': 'Buscar por tenencia...', + 'rpt.noCredentials': 'No se encontraron credenciales', + 'rpt.allRegionsDefault': 'Todas las regiones (predeterminado)', + 'rpt.searchRegion': 'Buscar region...', + 'rpt.loadingDashboard': 'Cargando dashboard...', + 'rpt.complianceScore': 'Puntuacion de Cumplimiento', + 'rpt.compliant': 'Conforme', + 'rpt.nonCompliant': 'No Conforme', + 'rpt.ofTotal': 'del total', + 'rpt.compliantItems': 'items conformes', + 'rpt.totalFindings': 'hallazgos totales', + 'rpt.evaluated': 'Evaluados', + 'rpt.controlsVerified': 'Controles verificados', + 'rpt.regionsCount': 'region(es)', + 'rpt.complianceDist': 'Distribucion de Cumplimiento', + 'rpt.findingsBySection': 'Hallazgos por Seccion', + 'rpt.generatedAt': 'Generado el', + 'rpt.htmlReport': 'Reporte HTML', + 'rpt.files': 'Archivos', + 'rpt.inSections': 'secciones', + 'rpt.fullEmbedding': 'Embedding Completo', + 'rpt.noActiveTables': 'Sin tablas activas', + 'rpt.executionHistory': 'Historial de Ejecuciones', + 'rpt.reports': 'reporte(s)', + 'rpt.loading': 'Cargando...', + 'rpt.firstScanHint': 'Ejecute el primer escaneo CIS arriba.', + 'rpt.selected': 'Seleccionado', + 'rpt.deleteConfirm': 'Eliminar este reporte?', + 'rpt.deleteFailed': 'Error al eliminar', + 'rpt.selectOciCredError': 'Seleccione una credencial OCI', + 'rpt.alreadyRunning': 'Ya hay un escaneo en ejecucion', + 'rpt.errorLoadReports': 'Error al cargar reportes', + 'rpt.errorStartScan': 'Error al iniciar escaneo', + 'rpt.selectAdb': 'Seleccione una conexion ADB.', + 'rpt.selectTable': 'Seleccione una tabla.', + 'rpt.errorEmbedding': 'Error al generar embedding', + 'rpt.cancelled': 'Cancelado', + 'rpt.completedAt': 'Completado:', + 'rpt.complianceGenerating': 'Generando Reporte de Cumplimiento...', + 'rpt.complianceFetchingKB': 'Obteniendo remediaciones de la Base de Conocimiento CIS', + 'rpt.complianceNotGenerated': 'Reporte aun no generado', + 'rpt.complianceGenerate': 'Generar Reporte de Cumplimiento', + 'rpt.complianceRegenerate': 'Regenerar', + 'rpt.complianceDownloadZip': 'Descargar ZIP', + 'rpt.complianceRemediation': '+ Remediacion', + 'rpt.complianceGeneratingShort': 'Generando...', + 'rpt.complianceTitle': 'LAD A-Team CIS Compliance Report', + + // ── OCI Config ── + 'oci.title': 'Credenciales OCI', + 'oci.subtitle': 'Administre las credenciales de Oracle Cloud Infrastructure', + 'oci.registered': 'Credenciales Registradas', + 'oci.credential': 'credencial', + 'oci.credentials': 'credenciales', + 'oci.noCredentials': 'No hay credenciales registradas.', + 'oci.edit': 'Editar', + 'oci.test': 'Probar', + 'oci.delete': 'Eliminar', + 'oci.yes': 'Si', + 'oci.no': 'No', + 'oci.newCredential': 'Nueva Credencial', + 'oci.editCredential': 'Editar Credencial', + 'oci.addDesc': 'Agregue las credenciales de su cuenta OCI para ejecutar reportes y acceder a servicios.', + 'oci.editing': 'Editando:', + 'oci.tenancyName': 'Nombre de Tenencia', + 'oci.ocidTenancy': 'OCID Tenencia', + 'oci.region': 'Region', + 'oci.selectRegion': 'Seleccione region...', + 'oci.searchRegion': 'Buscar region...', + 'oci.noRegion': 'No se encontraron regiones', + 'oci.compartment': 'OCID Compartimiento', + 'oci.ocidUser': 'OCID Usuario', + 'oci.fingerprint': 'Fingerprint', + 'oci.sessionToken': 'Token de Sesion', + 'oci.sessionTokenHint': 'Pegue el contenido del archivo de token generado por', + 'oci.sessionKeyLabel': 'Clave de Sesion (.pem)', + 'oci.privateKey': 'Clave Privada (.pem)', + 'oci.publicKey': 'Clave Publica (.pem)', + 'oci.keyPassphrase': 'Frase de Paso de la Clave', + 'oci.keyPassphraseHint': 'Solo si la clave privada esta protegida con contrasena', + 'oci.sshPublicKey': 'Clave Publica SSH', + 'oci.sshPublicKeyHint': 'Clave SSH para acceso a instancias de computo (ej. ssh-rsa AAAA...)', + 'oci.saveCredential': 'Guardar Credencial', + 'oci.saveChanges': 'Guardar Cambios', + 'oci.cancel': 'Cancelar', + 'oci.saving': 'Guardando credencial...', + 'oci.saved': 'Credencial guardada exitosamente!', + 'oci.updated': 'Credencial actualizada exitosamente!', + 'oci.deleting': 'Eliminando credencial...', + 'oci.deleted': 'Credencial eliminada exitosamente!', + 'oci.fillField': 'Complete', + 'oci.fillOcidTenancy': 'Complete OCID Tenencia', + 'oci.fillUserAndFP': 'Complete OCID Usuario y Fingerprint', + 'oci.pasteToken': 'Pegue el Token de Sesion', + 'oci.selectKey': 'Seleccione la clave privada (.pem)', + 'oci.selectSessionKey': 'Seleccione la clave de sesion (.pem)', + 'oci.changeHint': 'Complete para cambiar. Actual:', + 'oci.keepKey': 'Deje vacio para mantener la clave actual', + 'oci.typeCannotChange': '(el tipo no se puede cambiar)', + 'oci.sshNotConfigured': 'no configurado', + + // ── GenAI Config ── + 'genai.title': 'Modelos GenAI', + 'genai.subtitle': 'Configure modelos de inteligencia artificial generativa para el Chat Agent', + 'genai.configured': 'Modelos Configurados', + 'genai.model': 'modelo', + 'genai.models': 'modelos', + 'genai.noModels': 'No hay modelos configurados.', + 'genai.newModel': 'Nuevo Modelo GenAI', + 'genai.editModel': 'Editar Modelo', + 'genai.configName': 'Nombre de Configuracion', + 'genai.ociCredential': 'Credencial OCI', + 'genai.modelSelect': 'Modelo', + 'genai.genaiRegion': 'Region GenAI', + 'genai.compartment': 'OCID Compartimiento', + 'genai.modelOcid': 'OCID del Modelo', + 'genai.modelOcidHint': 'Pegue el OCID del modelo desde el portal OCI (opcional)', + 'genai.customOcid': 'Personalizado (usar OCID)', + 'genai.default': 'Modelo Predeterminado', + 'genai.setDefault': 'Establecer como Predeterminado', + 'genai.saveModel': 'Guardar Modelo', + 'genai.saveChanges': 'Guardar Cambios', + 'genai.cancel': 'Cancelar', + 'genai.saving': 'Guardando modelo...', + 'genai.deleting': 'Eliminando modelo...', + 'genai.addDesc': 'Agregue modelos personalizados o cree presets con credenciales especificas.', + 'genai.fillName': 'Complete el nombre de configuracion', + 'genai.selectOci': 'Seleccione una credencial OCI', + 'genai.fillOcid': 'Ingrese el OCID del modelo para modelo personalizado', + + // ── MCP Servers ── + 'mcp.title': 'Servidores MCP', + 'mcp.subtitle': 'Servidores MCP para integracion de herramientas y ejecucion de tareas', + 'mcp.registered': 'Servidores Registrados', + 'mcp.noServers': 'No hay servidores MCP registrados.', + 'mcp.edit': 'Editar', + 'mcp.activate': 'Activar', + 'mcp.deactivate': 'Desactivar', + 'mcp.active': 'Activo', + 'mcp.inactive': 'Inactivo', + 'mcp.delete': 'Eliminar', + 'mcp.yes': 'Si', + 'mcp.no': 'No', + 'mcp.command': 'Comando:', + 'mcp.url': 'URL:', + 'mcp.module': 'Modulo:', + 'mcp.adb': 'ADB:', + 'mcp.noAdb': 'Ninguno', + 'mcp.tools': 'Herramientas', + 'mcp.noTools': 'Sin herramientas registradas', + 'mcp.discoverTools': 'Descubrir Herramientas', + 'mcp.discovering': 'Conectando al servidor MCP para descubrir herramientas...', + 'mcp.newServer': 'Registrar Nuevo Servidor', + 'mcp.editServer': 'Editar Servidor', + 'mcp.name': 'Nombre', + 'mcp.description': 'Descripcion', + 'mcp.serverType': 'Tipo de Servidor', + 'mcp.commandLabel': 'Comando', + 'mcp.commandHint': 'Comando para iniciar el servidor', + 'mcp.argsLabel': 'Argumentos (JSON)', + 'mcp.argsHint': 'Arreglo de argumentos pasados al comando', + 'mcp.urlLabel': 'URL del Servidor', + 'mcp.urlHint': 'Endpoint SSE del servidor MCP', + 'mcp.linkAdb': 'Vincular ADB Vector', + 'mcp.linkAdbHint': 'Opcional — el servidor MCP puede usar esta base de datos como herramienta.', + 'mcp.saveServer': 'Registrar Servidor', + 'mcp.saveChanges': 'Guardar Cambios', + 'mcp.cancel': 'Cancelar', + 'mcp.saving': 'Registrando servidor...', + 'mcp.updating': 'Actualizando servidor...', + 'mcp.fillName': 'Complete el nombre del servidor', + 'mcp.invalidArgs': 'Los argumentos deben ser un arreglo JSON valido', + 'mcp.configDesc': 'Configure un nuevo servidor MCP. Los campos se ajustan segun el tipo seleccionado.', + + // ── ADB Config ── + 'adb.title': 'Conexiones ADB', + 'adb.subtitle': 'Conexiones a Oracle Autonomous Database via Wallet (mTLS)', + 'adb.registered': 'Conexiones Registradas', + 'adb.connection': 'conexion', + 'adb.connections': 'conexiones', + 'adb.noConnections': 'No hay conexiones ADB registradas.', + 'adb.edit': 'Editar', + 'adb.test': 'Probar', + 'adb.delete': 'Eliminar', + 'adb.yes': 'Si', + 'adb.no': 'No', + 'adb.newConnection': 'Nueva Conexion', + 'adb.editConnection': 'Editar Conexion', + 'adb.connName': 'Nombre de Conexion', + 'adb.walletZip': 'Wallet ZIP', + 'adb.walletHint': 'Suba el wallet para extraer DSNs de tnsnames.ora', + 'adb.walletEditHint': 'Suba un nuevo wallet o deje vacio para mantener el actual', + 'adb.analyze': 'Analizar', + 'adb.dsn': 'DSN (Nombre TNS)', + 'adb.dsnHint': 'Seleccione o escriba el DSN. Ej. myatp_high', + 'adb.username': 'Usuario', + 'adb.password': 'Contrasena', + 'adb.walletPassword': 'Contrasena del Wallet', + 'adb.genaiConfig': 'Configuracion GenAI (Embeddings)', + 'adb.genaiConfigHint': 'Credenciales OCI utilizadas para generar embeddings via GenAI.', + 'adb.noRag': 'Ninguno (sin RAG)', + 'adb.embeddingModel': 'Modelo de Embedding', + 'adb.embeddingModelHint': 'Modelo OCI GenAI para generar vectores.', + 'adb.saveConnection': 'Guardar Conexion', + 'adb.saveChanges': 'Guardar Cambios', + 'adb.cancel': 'Cancelar', + 'adb.saving': 'Guardando conexion...', + 'adb.deleting': 'Eliminando conexion...', + 'adb.updateWallet': 'Actualizar Wallet', + 'adb.adbConnection': 'Conexion ADB', + 'adb.uploadWallet': 'Subir Wallet', + 'adb.connDesc': 'Conexion via python-oracledb modo Thin con Wallet (mTLS) a Oracle Autonomous Database.', + 'adb.selectWallet': 'Seleccione un archivo ZIP de wallet.', + 'adb.selectConnection': 'Seleccione una conexion ADB.', + 'adb.fillName': 'Complete el nombre de la conexion', + 'adb.fillDsn': 'Complete el DSN', + 'adb.fillUsername': 'Complete el usuario', + 'adb.fillPassword': 'Complete la contrasena', + + // ── Embeddings ── + 'emb.title': 'Embeddings', + 'emb.subtitle': 'Base de conocimiento vectorial — carga, importacion y gestion', + 'emb.noAdb': 'No hay conexion ADB configurada.', + 'emb.noAdbHint': 'Configure en ADB Vector.', + 'emb.noOci': 'No hay credencial OCI configurada.', + 'emb.noOciHint': 'Configure en Config → OCI.', + 'emb.existing': 'Embeddings Existentes', + 'emb.existingDesc': 'Consulte documentos almacenados en las tablas de embedding de ADB.', + 'emb.adbConnection': 'Conexion ADB', + 'emb.table': 'Tabla', + 'emb.load': 'Cargar', + 'emb.noEmbeddings': 'No se encontraron embeddings.', + 'emb.total': 'Total: {0} documentos', + 'emb.cisTitle': 'Recomendaciones CIS', + 'emb.cisDesc': 'Suba la ultima version del PDF para mantener actualizadas las recomendaciones CIS de Oracle Cloud en la base de datos vectorial.', + 'emb.pdfFile': 'Archivo PDF', + 'emb.uploadCis': 'Subir Recomendaciones CIS', + 'emb.kbTitle': 'Base de Conocimiento', + 'emb.kbDesc': 'Suba documentos para alimentar la base de conocimiento del equipo. Los archivos seran vectorizados y estaran disponibles para consultas en el chat.', + 'emb.fileLabel': 'Archivo (.txt, .pdf, .csv, .json, .md)', + 'emb.uploadFile': 'Subir Archivo', + 'emb.urlLabel': 'URL (pagina web o PDF)', + 'emb.importUrl': 'Importar URL', + 'emb.purgeTitle': 'Purgar y Re-embeber', + 'emb.purgeDesc': 'Limpie los embeddings de una tabla para liberar espacio o preparar para re-ingestion. Esta accion es irreversible.', + 'emb.tenancyOptional': 'Tenencia (opcional)', + 'emb.purge': 'Purgar', + 'emb.confirm': 'Confirmar', + 'emb.selectTable': 'Seleccione una tabla.', + 'emb.selectAdbAndFile': 'Seleccione la conexion ADB y el archivo.', + 'emb.selectAdbAndUrl': 'Seleccione la conexion ADB e ingrese la URL.', + 'emb.processing': 'Procesando PDF y generando embeddings...', + 'emb.uploading': 'Subiendo y generando embeddings...', + 'emb.fetchingUrl': 'Obteniendo contenido y generando embeddings...', + 'emb.purging': 'Limpiando embeddings...', + + // ── EmbConsult ── + 'ec.title': 'Consultar Embeddings', + 'ec.subtitle': 'Consulte datos almacenados en bases de datos vectoriales', + 'ec.clear': 'Limpiar', + 'ec.tableOptional': 'Tabla (opcional)', + 'ec.allTables': 'Todas las tablas', + 'ec.emptyTitle': 'Realice una consulta para buscar embeddings.', + 'ec.emptyHint': 'Ej. "hallazgos para CIS 1.1", "recomendaciones de red", "como corregir MFA"', + 'ec.searching': 'Consultando bases de datos vectoriales...', + 'ec.placeholder': 'Escriba su consulta...', + 'ec.submit': 'Consultar', + 'ec.sources': 'Fuentes consultadas', + 'ec.noAnswer': 'Sin respuesta del modelo.', + + // ── Users ── + 'usr.title': 'Usuarios', + 'usr.activeCount': 'activo', + 'usr.activesCount': 'activos', + 'usr.inactiveCount': 'inactivo', + 'usr.inactivesCount': 'inactivos', + 'usr.total': 'total', + 'usr.newUser': 'Nuevo Usuario', + 'usr.cancel': 'Cancelar', + 'usr.createUser': 'Crear Nuevo Usuario', + 'usr.firstName': 'Nombre', + 'usr.lastName': 'Apellido', + 'usr.username': 'Usuario', + 'usr.email': 'Correo Electronico', + 'usr.password': 'Contrasena', + 'usr.role': 'Rol', + 'usr.creating': 'Creando...', + 'usr.create': 'Crear Usuario', + 'usr.noUsers': 'No hay usuarios registrados', + 'usr.noUsersHint': 'Haga clic en "Nuevo Usuario" para crear el primer usuario.', + 'usr.name': 'Nombre', + 'usr.mfa': 'MFA', + 'usr.status': 'Estado', + 'usr.lastLogin': 'Ultimo Ingreso', + 'usr.actions': 'Acciones', + 'usr.active': 'Activo', + 'usr.inactive': 'Inactivo', + 'usr.never': 'Nunca', + 'usr.confirm': 'Confirmar', + 'usr.deactivate': 'Desactivar usuario', + 'usr.created': 'Usuario creado exitosamente!', + 'usr.roleUpdated': 'Rol actualizado', + 'usr.deactivated': 'Usuario desactivado', + 'usr.requiredName': 'Nombre y apellido son requeridos', + 'usr.requiredUsername': 'El usuario es requerido', + 'usr.requiredPassword': 'La contrasena es requerida', + + // ── MFA ── + 'mfa.title': 'Autenticacion MFA (TOTP)', + 'mfa.subtitle': 'Proteja su cuenta con autenticacion de dos factores via Google Authenticator o Authy.', + 'mfa.enabled': 'MFA esta habilitado', + 'mfa.disabled': 'MFA esta deshabilitado', + 'mfa.enabledDesc': 'Su cuenta esta protegida con autenticacion de dos factores.', + 'mfa.disabledDesc': 'Habilite MFA para agregar una capa adicional de seguridad.', + 'mfa.disableHint': 'Para deshabilitar MFA, haga clic en el boton de abajo. Esto reducira la seguridad de su cuenta.', + 'mfa.disableBtn': 'Deshabilitar MFA', + 'mfa.confirmDisable': 'Esta seguro de que desea deshabilitar MFA para su cuenta?', + 'mfa.scanQr': 'Escanee el Codigo QR o copie el secreto', + 'mfa.openApp': 'Abra su aplicacion de autenticacion (Google Authenticator, Authy, etc.)', + 'mfa.enterCode': 'Ingrese el codigo de su aplicacion', + 'mfa.activateMfa': 'Activar MFA', + 'mfa.generateSecret': 'Generar Secreto', + 'mfa.generateHint': 'Haga clic en el boton de abajo para generar un secreto TOTP. Necesitara una aplicacion de autenticacion para escanear el Codigo QR.', + 'mfa.activated': 'MFA activado exitosamente!', + 'mfa.deactivated': 'MFA desactivado.', + 'mfa.invalidCode': 'Ingrese un codigo de 6 digitos', + 'mfa.configure': 'Configurar', + + // ── Audit ── + 'audit.title': 'Registro de Auditoria', + 'audit.subtitle': 'Monitoreo de actividad del sistema, configuraciones y sesiones de chat.', + 'audit.tabAudit': 'Registro de Auditoria', + 'audit.tabConfig': 'Logs de Configuracion', + 'audit.tabChat': 'Logs de Chat', + 'audit.records': 'registros', + 'audit.refresh': 'Actualizar', + 'audit.all': 'Todos', + 'audit.errors': 'Errores', + 'audit.success': 'Exitoso', + 'audit.info': 'Info', + 'audit.loadingLogs': 'Cargando logs...', + 'audit.noAudit': 'Sin registros de auditoria.', + 'audit.noConfig': 'Sin logs de configuracion registrados.', + 'audit.noChat': 'Sin logs de chat registrados.', + 'audit.date': 'Fecha', + 'audit.user': 'Usuario', + 'audit.action': 'Accion', + 'audit.resource': 'Recurso', + 'audit.details': 'Detalles', + 'audit.ip': 'IP', + 'audit.config': 'Configuracion', + 'audit.status': 'Estado', + 'audit.message': 'Mensaje', + 'audit.session': 'Sesion', + 'audit.source': 'Fuente', + + // ── Hardcoded fixes ── + 'common.errorPrefix': 'Error: ', + 'common.errorSave': 'Error al guardar', + 'common.errorDelete': 'Error al eliminar', + 'common.errorUnknown': 'Error desconocido', + 'common.errorLoad': 'Error al cargar', + + 'oci.thTenancy': 'Tenencia', + 'oci.thType': 'Tipo', + 'oci.thRegion': 'Region', + 'oci.thDetails': 'Detalles', + 'oci.thActions': 'Acciones', + + 'genai.thConfig': 'Configuracion', + 'genai.thModel': 'Modelo', + 'genai.thOciConfig': 'Configuracion OCI', + 'genai.thRegion': 'Region', + 'genai.thActions': 'Acciones', + 'genai.savedSuccess': 'Modelo guardado exitosamente!', + 'genai.updatedSuccess': 'Modelo actualizado exitosamente!', + 'genai.deletedSuccess': 'Modelo eliminado exitosamente!', + 'genai.editingLabel': 'Editando:', + 'genai.regionFilled': 'Region y Compartimiento completados desde {0}', + + 'mcp.savedSuccess': 'Servidor registrado exitosamente!', + 'mcp.updatedSuccess': 'Servidor actualizado exitosamente!', + 'mcp.deletingServer': 'Eliminando servidor...', + 'mcp.deletedSuccess': 'Servidor eliminado exitosamente!', + 'mcp.discoveredTools': '{0} herramienta(s) descubierta(s), {1} total', + 'mcp.errorDiscoverTools': 'Error al descubrir herramientas', + 'mcp.removeToolConfirm': 'Eliminar herramienta "{0}"?', + 'mcp.editingLabel': 'Editando:', + + 'adb.thName': 'Nombre', + 'adb.thDsn': 'DSN', + 'adb.thUser': 'Usuario', + 'adb.thTables': 'Tablas', + 'adb.thMtls': 'mTLS', + 'adb.thWallet': 'Wallet', + 'adb.thEmbed': 'Embed', + 'adb.thActions': 'Acciones', + 'adb.thTable': 'Tabla', + 'adb.thDescription': 'Descripcion', + 'adb.thActive': 'Activo', + 'adb.thTableActions': 'Acciones', + 'adb.walletParsed': 'Wallet analizado! {0} DSN(s): {1}. Archivos: {2}', + 'adb.savedSuccess': 'Conexion guardada!', + 'adb.updatedSuccess': 'Conexion actualizada!', + 'adb.walletIncluded': ' Wallet incluido.', + 'adb.deletedSuccess': 'Conexion eliminada exitosamente!', + 'adb.walletSending': 'Enviando wallet...', + 'adb.walletSent': 'Wallet subido! Archivos: {0}', + 'adb.enterTableName': 'Ingrese el nombre de la tabla.', + 'adb.tableRegistered': 'Tabla {0} registrada!', + 'adb.confirmRemoveTable': 'Eliminar esta tabla del registro?', + 'adb.tableRemoved': 'Tabla eliminada.', + 'adb.editingLabel': 'Editando:', + + 'emb.errorLoadEmb': 'Error al cargar embeddings', + 'emb.confirmDeleteEmb': 'Eliminar este embedding?', + 'emb.totalDocs': 'Total: {0} documentos', + + 'ec.errorPrefix': 'Error: ', + 'ec.errorUnknown': 'Error desconocido', + 'ec.docs': 'documentos', + + 'usr.errorLoadUsers': 'Error al cargar usuarios', + 'usr.errorCreateUser': 'Error al crear usuario', + 'usr.errorUpdateRole': 'Error al actualizar rol', + 'usr.errorDeactivate': 'Error al desactivar usuario', + 'usr.mfaSelfOnly': 'MFA solo puede ser activado por el propio usuario.', + 'usr.myTimezone': 'Mi Zona Horaria', + + 'ws.typeDestroyLabel': 'Escriba DESTROY para confirmar:', + + 'exp.tempAccess': 'Acceso temporal - ', + 'exp.optional': 'Opcional', + 'exp.ipPrompt': 'IP para permitir acceso (CIDR):', + 'exp.nsgType': 'Tipo', + + // ── Common ── + 'common.save': 'Guardar', + 'common.cancel': 'Cancelar', + 'common.delete': 'Eliminar', + 'common.edit': 'Editar', + 'common.create': 'Crear', + 'common.update': 'Actualizar', + 'common.close': 'Cerrar', + 'common.confirm': 'Confirmar', + 'common.back': 'Volver', + 'common.search': 'Buscar', + 'common.refresh': 'Actualizar', + 'common.download': 'Descargar', + 'common.upload': 'Subir', + 'common.copy': 'Copiar', + 'common.retry': 'Reintentar', + 'common.send': 'Enviar', + 'common.clear': 'Limpiar', + 'common.test': 'Probar', + 'common.yes': 'Si', + 'common.no': 'No', + 'common.loading': 'Cargando...', + 'common.saving': 'Guardando...', + 'common.select': 'Seleccione...', + 'common.noData': 'No se encontraron datos', + 'common.error': 'Error', + 'common.success': 'Exitoso', + // ── My Settings ── + 'settings.title': 'Mi Configuracion', + 'settings.timezone': 'Zona Horaria', + 'settings.tzDesc': 'Establezca su zona horaria para la visualizacion de fecha y hora.', + 'settings.tzChanged': 'Zona horaria cambiada exitosamente', + 'settings.language': 'Idioma', + 'settings.langDesc': 'Seleccione el idioma de la interfaz.', + 'settings.changePassword': 'Cambiar Contrasena', + 'settings.currentPw': 'Contrasena Actual', + 'settings.newPw': 'Nueva Contrasena', + 'settings.confirmPw': 'Confirmar Nueva Contrasena', + 'settings.savePw': 'Cambiar Contrasena', + 'settings.saving': 'Guardando...', + 'settings.pwChanged': 'Contrasena cambiada exitosamente', + 'settings.pwRequired': 'Complete todos los campos', + 'settings.pwMismatch': 'Las contrasenas no coinciden', + 'settings.pwMinLength': 'La nueva contrasena debe tener al menos 8 caracteres', + 'settings.federatedInfo': 'La contrasena y MFA son administrados por Oracle IAM Identity Domain. Configure directamente en el portal OCI.', + 'settings.mfa': 'Autenticacion Multi-Factor (MFA)', + // ── Terminal ── + 'term.title': 'OCI CLI Terminal', + 'term.subtitle': 'Ejecute comandos OCI CLI directamente en el navegador', + 'term.config': 'Configuración', + 'term.selectConfig': 'Seleccione una configuración OCI', + 'term.welcome': 'Escriba comandos OCI CLI. Ej: oci iam user list --compartment-id ', + 'term.clearHint': 'para limpiar', + 'term.historyHint': 'para navegar en el historial', + 'term.executing': 'Ejecutando...', + 'term.noOutput': 'Comando ejecutado sin output', + // ── Oracle IAM ── + 'iam.title': 'Oracle IAM Identity Domains', + 'iam.subtitle': 'Integracion OIDC para autenticacion corporativa', + 'iam.authMode': 'Modo de Autenticacion', + 'iam.authModeDesc': 'Define como los usuarios se autentican en el sistema.', + 'iam.modeLocal': 'Local', + 'iam.modeLocalDesc': 'Usuarios locales con contrasena', + 'iam.modeOidc': 'Oracle IAM', + 'iam.modeOidcDesc': 'Solo SSO', + 'iam.modeBoth': 'Ambos', + 'iam.modeBothDesc': 'Local + Oracle IAM', + 'iam.oidcConfig': 'Configuracion OIDC', + 'iam.testConnection': 'Probar Conexion', + 'iam.issuerUrl': 'URL del Emisor (Identity Domain)', + 'iam.clientId': 'Client ID', + 'iam.clientSecret': 'Client Secret', + 'iam.redirectUri': 'URI de Redireccion', + 'iam.groupMapping': 'Mapeo de Grupos', + 'iam.groupMappingDesc': 'Mapee los grupos de OCI Identity Domain a roles del sistema.', + 'iam.save': 'Guardar Configuracion', + 'iam.saving': 'Guardando...', + 'iam.saved': 'Configuracion guardada exitosamente', + 'iam.issuerRequired': 'La URL del emisor es requerida', + 'iam.testOk': 'Conexion OK', + 'iam.testInvalid': 'Respuesta invalida del endpoint de descubrimiento', + 'iam.testFail': 'Fallo en la conexion', +}; + +export default es; diff --git a/frontend-react/src/i18n/index.ts b/frontend-react/src/i18n/index.ts index 1e8d188..c8acbcf 100644 --- a/frontend-react/src/i18n/index.ts +++ b/frontend-react/src/i18n/index.ts @@ -1,15 +1,19 @@ import { create } from 'zustand'; import pt from './pt'; import en from './en'; +import es from './es'; -type Lang = 'pt' | 'en'; +type Lang = 'pt' | 'en' | 'es'; -const LANGS: Record> = { pt, en }; +const LANGS: Record> = { pt, en, es }; function detectLang(): Lang { const saved = localStorage.getItem('lang') as Lang | null; if (saved && LANGS[saved]) return saved; - return navigator.language.startsWith('pt') ? 'pt' : 'en'; + const nav = navigator.language.toLowerCase(); + if (nav.startsWith('pt')) return 'pt'; + if (nav.startsWith('es')) return 'es'; + return 'en'; } interface I18nState { diff --git a/frontend-react/src/i18n/pt.ts b/frontend-react/src/i18n/pt.ts index f78954a..96c2af5 100644 --- a/frontend-react/src/i18n/pt.ts +++ b/frontend-react/src/i18n/pt.ts @@ -20,6 +20,7 @@ const pt: Record = { 'nav.adb': 'ADB Vector', 'nav.embeddings': 'Embeddings', 'nav.embConsult': 'Consultar Embeddings', + 'nav.terminal': 'Terminal', 'nav.userMgmt': 'Gestão de Usuários', 'nav.users': 'Usuários', 'nav.mySettings': 'Minhas Configurações', @@ -756,6 +757,8 @@ const pt: Record = { 'settings.timezone': 'Timezone', 'settings.tzDesc': 'Define o fuso horário para exibição de datas e horários.', 'settings.tzChanged': 'Timezone alterado com sucesso', + 'settings.language': 'Idioma', + 'settings.langDesc': 'Selecione o idioma da interface.', 'settings.changePassword': 'Alterar Senha', 'settings.currentPw': 'Senha Atual', 'settings.newPw': 'Nova Senha', @@ -768,6 +771,16 @@ const pt: Record = { 'settings.pwMinLength': 'A nova senha deve ter pelo menos 8 caracteres', 'settings.federatedInfo': 'Senha e MFA são gerenciados pelo Oracle IAM Identity Domain. Configure diretamente no portal OCI.', 'settings.mfa': 'Autenticação Multi-Fator (MFA)', + // ── Terminal ── + 'term.title': 'OCI CLI Terminal', + 'term.subtitle': 'Execute comandos OCI CLI diretamente no navegador', + 'term.config': 'Configuração', + 'term.selectConfig': 'Selecione uma configuração OCI', + 'term.welcome': 'Digite comandos OCI CLI. Ex: oci iam user list --compartment-id ', + 'term.clearHint': 'para limpar', + 'term.historyHint': 'para navegar no histórico', + 'term.executing': 'Executando...', + 'term.noOutput': 'Comando executado sem output', // ── Oracle IAM ── 'iam.title': 'Oracle IAM Identity Domains', 'iam.subtitle': 'Integração OIDC para autenticação corporativa', diff --git a/frontend-react/src/pages/TerminalPage.tsx b/frontend-react/src/pages/TerminalPage.tsx new file mode 100644 index 0000000..ff92b0c --- /dev/null +++ b/frontend-react/src/pages/TerminalPage.tsx @@ -0,0 +1,339 @@ +import { useState, useEffect, useRef, useCallback } from 'react'; +import { Terminal, Loader2, ChevronDown } from 'lucide-react'; +import client from '@/api/client'; +import { useI18n } from '@/i18n'; + +interface TermLine { + type: 'input' | 'output' | 'error' | 'info'; + text: string; +} + +interface OciCfg { + id: string; + tenancy_name: string; +} + +export default function TerminalPage() { + const { t } = useI18n(); + const [configs, setConfigs] = useState([]); + const [selectedCfg, setSelectedCfg] = useState(''); + const [command, setCommand] = useState(''); + const [lines, setLines] = useState([]); + const [running, setRunning] = useState(false); + const [cmdHistory, setCmdHistory] = useState([]); + const [histIdx, setHistIdx] = useState(-1); + const [suggestions, setSuggestions] = useState([]); + const [tabCount, setTabCount] = useState(0); + const outputRef = useRef(null); + const inputRef = useRef(null); + + useEffect(() => { + client.get('/oci/configs').then((data: any) => { + setConfigs(data); + if (data.length > 0 && !selectedCfg) setSelectedCfg(data[0].id); + }).catch(() => {}); + }, []); + + useEffect(() => { + if (outputRef.current) outputRef.current.scrollTop = outputRef.current.scrollHeight; + }, [lines]); + + useEffect(() => { + if (selectedCfg) { + client.get(`/terminal/history?oci_config_id=${selectedCfg}&limit=100`).then((data: any) => { + setCmdHistory(data.map((h: any) => h.command).reverse()); + }).catch(() => {}); + } + }, [selectedCfg]); + + const addLine = useCallback((type: TermLine['type'], text: string) => { + setLines(prev => [...prev, { type, text }]); + }, []); + + const execute = useCallback(async () => { + const cmd = command.trim(); + if (!cmd) return; + if (!selectedCfg) { addLine('error', t('term.selectConfig')); return; } + + // Built-in commands + if (cmd === 'clear') { setLines([]); setCommand(''); return; } + if (cmd === 'history') { + addLine('input', `$ ${cmd}`); + cmdHistory.forEach((h, i) => addLine('output', ` ${i + 1} ${h}`)); + setCommand(''); + return; + } + if (cmd === 'help') { + addLine('input', `$ ${cmd}`); + addLine('info', 'OCI CLI Terminal — Comandos disponíveis:'); + addLine('info', ' oci [options] Executar comando OCI CLI'); + addLine('info', ' Tab Autocomplete'); + addLine('info', ' ↑ / ↓ Navegar histórico'); + addLine('info', ' clear Limpar terminal'); + addLine('info', ' history Ver histórico'); + addLine('info', ' help Este menu'); + setCommand(''); + return; + } + + addLine('input', `$ ${cmd}`); + setCommand(''); + setCmdHistory(prev => [...prev, cmd]); + setHistIdx(-1); + setSuggestions([]); + setRunning(true); + + try { + const resp = await client.post('/terminal/execute', { + oci_config_id: selectedCfg, + command: cmd, + }) as any; + if (resp.output) addLine('output', resp.output); + if (resp.error) addLine('error', resp.error); + if (!resp.output && !resp.error) addLine('info', '(no output)'); + } catch (err: any) { + addLine('error', err?.message || err?.detail || String(err)); + } finally { + setRunning(false); + inputRef.current?.focus(); + } + }, [command, selectedCfg, addLine, cmdHistory, t]); + + const handleTab = useCallback(async () => { + if (!command.trim() || running) return; + try { + const resp = await client.get(`/terminal/completions?prefix=${encodeURIComponent(command)}`) as any; + const sugs: string[] = resp.suggestions || []; + if (sugs.length === 0) return; + if (sugs.length === 1) { + // Single match — auto-complete + const parts = command.trimEnd().split(' '); + if (command.endsWith(' ')) { + setCommand(command + sugs[0] + ' '); + } else { + parts[parts.length - 1] = sugs[0]; + setCommand(parts.join(' ') + ' '); + } + setSuggestions([]); + setTabCount(0); + } else { + // Multiple matches — show them + if (tabCount > 0) { + // Second Tab — display all + addLine('input', `$ ${command}`); + addLine('info', sugs.join(' ')); + } + setSuggestions(sugs); + setTabCount(prev => prev + 1); + // Find common prefix + let common = sugs[0]; + for (const s of sugs) { + while (!s.startsWith(common)) common = common.slice(0, -1); + } + if (common) { + const parts = command.trimEnd().split(' '); + const last = parts[parts.length - 1]; + if (command.endsWith(' ')) { + if (common.length > 0) setCommand(command + common); + } else if (common.length > last.length) { + parts[parts.length - 1] = common; + setCommand(parts.join(' ')); + } + } + } + } catch { + // ignore + } + }, [command, running, tabCount, addLine]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Tab') { + e.preventDefault(); + handleTab(); + return; + } + setTabCount(0); + setSuggestions([]); + + if (e.key === 'Enter' && !running) { + execute(); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + if (cmdHistory.length > 0) { + const newIdx = histIdx < cmdHistory.length - 1 ? histIdx + 1 : histIdx; + setHistIdx(newIdx); + setCommand(cmdHistory[cmdHistory.length - 1 - newIdx] || ''); + } + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + if (histIdx > 0) { + setHistIdx(histIdx - 1); + setCommand(cmdHistory[cmdHistory.length - histIdx] || ''); + } else { + setHistIdx(-1); + setCommand(''); + } + } else if (e.key === 'l' && e.ctrlKey) { + e.preventDefault(); + setLines([]); + } else if (e.key === 'c' && e.ctrlKey && running) { + addLine('error', '^C'); + } + }; + + const cfgName = configs.find(c => c.id === selectedCfg)?.tenancy_name || ''; + const prompt = `agent@${cfgName || 'oci'}:~$ `; + + return ( +
+ {/* Header bar */} +
+
+ + OCI CLI +
+
+ + +
+
+ + Tab: autocomplete | ↑↓: history | Ctrl+L: clear + +
+ + {/* Terminal window */} +
inputRef.current?.focus()} + > + {/* Title bar */} +
+
+
+
+
+
+ + agent@oci-cis-agent — {cfgName || 'terminal'} + +
+ + {/* Output */} +
+ {/* MOTD */} + {lines.length === 0 && ( +
+
{`
+  ___   ____ ___    ____ _     ___
+ / _ \\ / ___|_ _|  / ___| |   |_ _|
+| | | | |    | |  | |   | |    | |
+| |_| | |___ | |  | |___| |___ | |
+ \\___/ \\____|___|  \\____|_____|___|
+`}
+ Oracle Cloud Infrastructure CLI v3.x +
+ Type help for available commands, Tab for autocomplete +

+
+ )} + + {lines.map((line, i) => ( +
+ {line.type === 'input' && ( + {prompt.split(':')[0]}:~$ {line.text.replace(/^\$ /, '')} + )} + {line.type === 'output' && ( + {line.text} + )} + {line.type === 'error' && ( + {line.text} + )} + {line.type === 'info' && ( + {line.text} + )} +
+ ))} + + {running && ( +
+ + {t('term.executing')} +
+ )} + + {/* Autocomplete suggestions inline */} + {suggestions.length > 1 && !running && ( +
+ {suggestions.join(' ')} +
+ )} +
+ + {/* Input line */} +
+ {prompt.split(':')[0]}: + ~ + $ + { setCommand(e.target.value); setTabCount(0); setSuggestions([]); }} + onKeyDown={handleKeyDown} + disabled={running} + placeholder={running ? '' : ''} + autoFocus + spellCheck={false} + autoComplete="off" + autoCapitalize="off" + className="flex-1 outline-none" + style={{ + background: 'transparent', + border: 'none', + color: '#e6edf3', + fontSize: 'inherit', + fontFamily: 'inherit', + lineHeight: 'inherit', + caretColor: '#e6edf3', + padding: 0, + }} + /> + {/* Blinking cursor when empty and not running */} + {!command && !running && ( + + )} +
+
+ + +
+ ); +} diff --git a/frontend-react/src/pages/admin/MySettingsPage.tsx b/frontend-react/src/pages/admin/MySettingsPage.tsx index 479102b..8b431ff 100644 --- a/frontend-react/src/pages/admin/MySettingsPage.tsx +++ b/frontend-react/src/pages/admin/MySettingsPage.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { Settings, Clock, ShieldCheck, ShieldOff, KeyRound, Lock, Loader2, - CheckCircle, AlertTriangle, Eye, EyeOff, Save, + CheckCircle, AlertTriangle, Eye, EyeOff, Save, Languages, } from 'lucide-react'; import client from '@/api/client'; import { adminApi } from '@/api/endpoints/admin'; @@ -9,7 +9,7 @@ import { useAuthStore } from '@/stores/auth'; import { useI18n } from '@/i18n'; export default function MySettingsPage() { - const { t } = useI18n(); + const { t, lang, setLang } = useI18n(); const { user: currentUser, checkAuth } = useAuthStore(); const [msg, setMsg] = useState<{ text: string; type: 'success' | 'error' } | null>(null); @@ -177,6 +177,33 @@ export default function MySettingsPage() {
+ {/* Language */} +
+
+
+ +
+

{t('settings.language') || 'Idioma'}

+
+

+ {t('settings.langDesc') || 'Selecione o idioma da interface.'} +

+
+ {([['pt', 'Português', '🇧🇷'], ['en', 'English', '🇺🇸'], ['es', 'Español', '🇪🇸']] as const).map(([code, label, flag]) => ( + + ))} +
+
+ {/* Password Change — only for local users */} {isLocal && (