feat: OCI CLI Terminal, Spanish i18n, language in My Settings
Terminal: - New /terminal page with Linux-style terminal UI (dark theme, title bar, prompt) - OCI Config selector — isolated per user (ownership check) - Tab autocomplete: parses oci --help dynamically, caches results, commands first - Command history (↑/↓) persisted in DB per user - Security: only 'oci' commands, blocks pipes/redirects/shell injection - Built-in commands: clear, history, help - Timeout 60s per command i18n: - Spanish (es.ts): 748+ keys, Latin American formal - Language selector moved from sidebar to My Settings (🇧🇷/🇺🇸/🇪🇸) - Auto-detect browser language (pt/es/en) Backend: - POST /api/terminal/execute — run OCI CLI with selected config - GET /api/terminal/completions — dynamic autocomplete from oci --help - GET /api/terminal/history — per-user command history - terminal_history table in SQLite
This commit is contained in:
145
backend/app.py
145
backend/app.py
@@ -323,6 +323,12 @@ def init_db():
|
|||||||
action TEXT NOT NULL, resource TEXT, details TEXT,
|
action TEXT NOT NULL, resource TEXT, details TEXT,
|
||||||
ip TEXT, created_at TEXT DEFAULT (datetime('now'))
|
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 (
|
CREATE TABLE IF NOT EXISTS config_logs (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
config_type TEXT NOT NULL,
|
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"])
|
_config_log("oci", cid, cname, "error", "test", str(e)[:500], u["id"], u["username"])
|
||||||
return {"status":"error","message":str(e)[:500]}
|
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 <args> --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 ──────────────────────────────────────────────────────
|
# ── OCI Account Explorer ──────────────────────────────────────────────────────
|
||||||
@app.get("/api/oci/explore/{cid}/compartments")
|
@app.get("/api/oci/explore/{cid}/compartments")
|
||||||
async def explore_compartments(cid: str, u=Depends(current_user)):
|
async def explore_compartments(cid: str, u=Depends(current_user)):
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const McpServersPage = lazy(() => import('@/pages/config/McpServersPage'));
|
|||||||
const AdbConfigPage = lazy(() => import('@/pages/config/AdbConfigPage'));
|
const AdbConfigPage = lazy(() => import('@/pages/config/AdbConfigPage'));
|
||||||
const EmbeddingsPage = lazy(() => import('@/pages/config/EmbeddingsPage'));
|
const EmbeddingsPage = lazy(() => import('@/pages/config/EmbeddingsPage'));
|
||||||
const EmbConsultPage = lazy(() => import('@/pages/config/EmbConsultPage'));
|
const EmbConsultPage = lazy(() => import('@/pages/config/EmbConsultPage'));
|
||||||
|
const TerminalPage = lazy(() => import('@/pages/TerminalPage'));
|
||||||
const UsersPage = lazy(() => import('@/pages/admin/UsersPage'));
|
const UsersPage = lazy(() => import('@/pages/admin/UsersPage'));
|
||||||
const MySettingsPage = lazy(() => import('@/pages/admin/MySettingsPage'));
|
const MySettingsPage = lazy(() => import('@/pages/admin/MySettingsPage'));
|
||||||
const OracleIamPage = lazy(() => import('@/pages/admin/OracleIamPage'));
|
const OracleIamPage = lazy(() => import('@/pages/admin/OracleIamPage'));
|
||||||
@@ -61,6 +62,7 @@ export default function App() {
|
|||||||
<Route path="/explorer" element={<ExplorerPage />} />
|
<Route path="/explorer" element={<ExplorerPage />} />
|
||||||
<Route path="/oci-services" element={<OciServicesPage />} />
|
<Route path="/oci-services" element={<OciServicesPage />} />
|
||||||
<Route path="/reports" element={<ReportsPage />} />
|
<Route path="/reports" element={<ReportsPage />} />
|
||||||
|
<Route path="/terminal" element={<TerminalPage />} />
|
||||||
<Route path="/config/oci" element={<OciConfigPage />} />
|
<Route path="/config/oci" element={<OciConfigPage />} />
|
||||||
<Route path="/config/genai" element={<GenAiConfigPage />} />
|
<Route path="/config/genai" element={<GenAiConfigPage />} />
|
||||||
<Route path="/config/mcp" element={<McpServersPage />} />
|
<Route path="/config/mcp" element={<McpServersPage />} />
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
MessageSquare, Search, BarChart3, Cloud, Shield,
|
MessageSquare, Search, BarChart3, Cloud, Shield, Terminal,
|
||||||
Brain, Plug, Database, Dna, Users, FileText,
|
Brain, Plug, Database, Dna, Users, FileText,
|
||||||
Sparkles, Sun, Moon, LogOut, FolderOpen, Languages, Settings, ArrowLeft,
|
Sparkles, Sun, Moon, LogOut, FolderOpen, Settings, ArrowLeft,
|
||||||
UserCog, ChevronDown, ChevronRight,
|
UserCog, ChevronDown, ChevronRight,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useTheme } from '@/hooks/useTheme';
|
import { useTheme } from '@/hooks/useTheme';
|
||||||
@@ -52,6 +52,7 @@ const NAV_ITEMS: NavItem[] = [
|
|||||||
{ to: '/oci-services', icon: <Shield size={SZ} />, labelKey: 'nav.ociServices' },
|
{ to: '/oci-services', icon: <Shield size={SZ} />, labelKey: 'nav.ociServices' },
|
||||||
{ to: '/reports', icon: <BarChart3 size={SZ} />, labelKey: 'nav.reports' },
|
{ to: '/reports', icon: <BarChart3 size={SZ} />, labelKey: 'nav.reports' },
|
||||||
{ to: '/config/embeddings/consult', icon: <MessageSquare size={SZ} />, labelKey: 'nav.embConsult' },
|
{ to: '/config/embeddings/consult', icon: <MessageSquare size={SZ} />, labelKey: 'nav.embConsult' },
|
||||||
|
{ to: '/terminal', icon: <Terminal size={SZ} />, labelKey: 'nav.terminal' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const ADMIN_ITEMS: NavItem[] = [
|
const ADMIN_ITEMS: NavItem[] = [
|
||||||
@@ -73,7 +74,7 @@ const USER_MGMT_ITEMS: NavItem[] = [
|
|||||||
export default function Sidebar() {
|
export default function Sidebar() {
|
||||||
const { toggle, isDark } = useTheme();
|
const { toggle, isDark } = useTheme();
|
||||||
const { user, logout } = useAuthStore();
|
const { user, logout } = useAuthStore();
|
||||||
const { t, lang, setLang } = useI18n();
|
const { t } = useI18n();
|
||||||
const [showSettings, setShowSettings] = useState(false);
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
const [userMgmtOpen, setUserMgmtOpen] = useState(false);
|
const [userMgmtOpen, setUserMgmtOpen] = useState(false);
|
||||||
|
|
||||||
@@ -196,18 +197,6 @@ export default function Sidebar() {
|
|||||||
>
|
>
|
||||||
{isDark ? <Sun size={18} /> : <Moon size={18} />}
|
{isDark ? <Sun size={18} /> : <Moon size={18} />}
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
onClick={() => 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'}
|
|
||||||
aria-label={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)'; }}
|
|
||||||
>
|
|
||||||
<Languages size={18} />
|
|
||||||
<span className="text-[.65rem] font-bold uppercase">{lang}</span>
|
|
||||||
</button>
|
|
||||||
<div className="flex-1 flex items-center gap-2 min-w-0">
|
<div className="flex-1 flex items-center gap-2 min-w-0">
|
||||||
<div className="w-7 h-7 rounded-lg flex items-center justify-center text-[.7rem] font-bold text-white flex-shrink-0"
|
<div className="w-7 h-7 rounded-lg flex items-center justify-center text-[.7rem] font-bold text-white flex-shrink-0"
|
||||||
style={{ background: 'var(--ac)' }}>
|
style={{ background: 'var(--ac)' }}>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const en: Record<string, string> = {
|
|||||||
'nav.adb': 'ADB Vector',
|
'nav.adb': 'ADB Vector',
|
||||||
'nav.embeddings': 'Embeddings',
|
'nav.embeddings': 'Embeddings',
|
||||||
'nav.embConsult': 'Query Embeddings',
|
'nav.embConsult': 'Query Embeddings',
|
||||||
|
'nav.terminal': 'Terminal',
|
||||||
'nav.userMgmt': 'User Management',
|
'nav.userMgmt': 'User Management',
|
||||||
'nav.users': 'Users',
|
'nav.users': 'Users',
|
||||||
'nav.mySettings': 'My Settings',
|
'nav.mySettings': 'My Settings',
|
||||||
@@ -755,6 +756,8 @@ const en: Record<string, string> = {
|
|||||||
'settings.timezone': 'Timezone',
|
'settings.timezone': 'Timezone',
|
||||||
'settings.tzDesc': 'Set your timezone for date and time display.',
|
'settings.tzDesc': 'Set your timezone for date and time display.',
|
||||||
'settings.tzChanged': 'Timezone changed successfully',
|
'settings.tzChanged': 'Timezone changed successfully',
|
||||||
|
'settings.language': 'Language',
|
||||||
|
'settings.langDesc': 'Select the interface language.',
|
||||||
'settings.changePassword': 'Change Password',
|
'settings.changePassword': 'Change Password',
|
||||||
'settings.currentPw': 'Current Password',
|
'settings.currentPw': 'Current Password',
|
||||||
'settings.newPw': 'New Password',
|
'settings.newPw': 'New Password',
|
||||||
@@ -767,6 +770,16 @@ const en: Record<string, string> = {
|
|||||||
'settings.pwMinLength': 'New password must be at least 8 characters',
|
'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.federatedInfo': 'Password and MFA are managed by Oracle IAM Identity Domain. Configure directly in the OCI portal.',
|
||||||
'settings.mfa': 'Multi-Factor Authentication (MFA)',
|
'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 <ocid>',
|
||||||
|
'term.clearHint': 'to clear',
|
||||||
|
'term.historyHint': 'to browse history',
|
||||||
|
'term.executing': 'Executing...',
|
||||||
|
'term.noOutput': 'Command executed with no output',
|
||||||
// ── Oracle IAM ──
|
// ── Oracle IAM ──
|
||||||
'iam.title': 'Oracle IAM Identity Domains',
|
'iam.title': 'Oracle IAM Identity Domains',
|
||||||
'iam.subtitle': 'OIDC integration for corporate authentication',
|
'iam.subtitle': 'OIDC integration for corporate authentication',
|
||||||
|
|||||||
811
frontend-react/src/i18n/es.ts
Normal file
811
frontend-react/src/i18n/es.ts
Normal file
@@ -0,0 +1,811 @@
|
|||||||
|
const es: Record<string, string> = {
|
||||||
|
// ── 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 <strong>DESTROY</strong> 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 <strong>{0}</strong>',
|
||||||
|
|
||||||
|
'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): <strong>{1}</strong>. 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 <strong>DESTROY</strong> 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 <ocid>',
|
||||||
|
'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;
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import pt from './pt';
|
import pt from './pt';
|
||||||
import en from './en';
|
import en from './en';
|
||||||
|
import es from './es';
|
||||||
|
|
||||||
type Lang = 'pt' | 'en';
|
type Lang = 'pt' | 'en' | 'es';
|
||||||
|
|
||||||
const LANGS: Record<Lang, Record<string, string>> = { pt, en };
|
const LANGS: Record<Lang, Record<string, string>> = { pt, en, es };
|
||||||
|
|
||||||
function detectLang(): Lang {
|
function detectLang(): Lang {
|
||||||
const saved = localStorage.getItem('lang') as Lang | null;
|
const saved = localStorage.getItem('lang') as Lang | null;
|
||||||
if (saved && LANGS[saved]) return saved;
|
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 {
|
interface I18nState {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const pt: Record<string, string> = {
|
|||||||
'nav.adb': 'ADB Vector',
|
'nav.adb': 'ADB Vector',
|
||||||
'nav.embeddings': 'Embeddings',
|
'nav.embeddings': 'Embeddings',
|
||||||
'nav.embConsult': 'Consultar Embeddings',
|
'nav.embConsult': 'Consultar Embeddings',
|
||||||
|
'nav.terminal': 'Terminal',
|
||||||
'nav.userMgmt': 'Gestão de Usuários',
|
'nav.userMgmt': 'Gestão de Usuários',
|
||||||
'nav.users': 'Usuários',
|
'nav.users': 'Usuários',
|
||||||
'nav.mySettings': 'Minhas Configurações',
|
'nav.mySettings': 'Minhas Configurações',
|
||||||
@@ -756,6 +757,8 @@ const pt: Record<string, string> = {
|
|||||||
'settings.timezone': 'Timezone',
|
'settings.timezone': 'Timezone',
|
||||||
'settings.tzDesc': 'Define o fuso horário para exibição de datas e horários.',
|
'settings.tzDesc': 'Define o fuso horário para exibição de datas e horários.',
|
||||||
'settings.tzChanged': 'Timezone alterado com sucesso',
|
'settings.tzChanged': 'Timezone alterado com sucesso',
|
||||||
|
'settings.language': 'Idioma',
|
||||||
|
'settings.langDesc': 'Selecione o idioma da interface.',
|
||||||
'settings.changePassword': 'Alterar Senha',
|
'settings.changePassword': 'Alterar Senha',
|
||||||
'settings.currentPw': 'Senha Atual',
|
'settings.currentPw': 'Senha Atual',
|
||||||
'settings.newPw': 'Nova Senha',
|
'settings.newPw': 'Nova Senha',
|
||||||
@@ -768,6 +771,16 @@ const pt: Record<string, string> = {
|
|||||||
'settings.pwMinLength': 'A nova senha deve ter pelo menos 8 caracteres',
|
'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.federatedInfo': 'Senha e MFA são gerenciados pelo Oracle IAM Identity Domain. Configure diretamente no portal OCI.',
|
||||||
'settings.mfa': 'Autenticação Multi-Fator (MFA)',
|
'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 <ocid>',
|
||||||
|
'term.clearHint': 'para limpar',
|
||||||
|
'term.historyHint': 'para navegar no histórico',
|
||||||
|
'term.executing': 'Executando...',
|
||||||
|
'term.noOutput': 'Comando executado sem output',
|
||||||
// ── Oracle IAM ──
|
// ── Oracle IAM ──
|
||||||
'iam.title': 'Oracle IAM Identity Domains',
|
'iam.title': 'Oracle IAM Identity Domains',
|
||||||
'iam.subtitle': 'Integração OIDC para autenticação corporativa',
|
'iam.subtitle': 'Integração OIDC para autenticação corporativa',
|
||||||
|
|||||||
339
frontend-react/src/pages/TerminalPage.tsx
Normal file
339
frontend-react/src/pages/TerminalPage.tsx
Normal file
@@ -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<OciCfg[]>([]);
|
||||||
|
const [selectedCfg, setSelectedCfg] = useState('');
|
||||||
|
const [command, setCommand] = useState('');
|
||||||
|
const [lines, setLines] = useState<TermLine[]>([]);
|
||||||
|
const [running, setRunning] = useState(false);
|
||||||
|
const [cmdHistory, setCmdHistory] = useState<string[]>([]);
|
||||||
|
const [histIdx, setHistIdx] = useState(-1);
|
||||||
|
const [suggestions, setSuggestions] = useState<string[]>([]);
|
||||||
|
const [tabCount, setTabCount] = useState(0);
|
||||||
|
const outputRef = useRef<HTMLDivElement>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(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 <service> <resource> <action> [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 (
|
||||||
|
<div className="page" style={{ display: 'flex', flexDirection: 'column', height: '100%', gap: 8 }}>
|
||||||
|
{/* Header bar */}
|
||||||
|
<div className="flex items-center gap-3" style={{ flexShrink: 0 }}>
|
||||||
|
<div className="flex items-center gap-2 px-3 py-2 rounded-lg" style={{ background: 'var(--bg2)', border: '1px solid var(--bd)' }}>
|
||||||
|
<Terminal size={16} style={{ color: 'var(--ac)' }} />
|
||||||
|
<span className="text-sm font-semibold" style={{ color: 'var(--t1)' }}>OCI CLI</span>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<select
|
||||||
|
value={selectedCfg}
|
||||||
|
onChange={e => { setSelectedCfg(e.target.value); setLines([]); }}
|
||||||
|
className="px-3 py-2 pr-8 rounded-lg text-sm font-medium outline-none cursor-pointer appearance-none"
|
||||||
|
style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t1)', minWidth: 200 }}
|
||||||
|
>
|
||||||
|
<option value="">-- {t('term.selectConfig')} --</option>
|
||||||
|
{configs.map(c => (
|
||||||
|
<option key={c.id} value={c.id}>{c.tenancy_name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<ChevronDown size={14} className="absolute right-2.5 top-1/2 -translate-y-1/2 pointer-events-none" style={{ color: 'var(--t3)' }} />
|
||||||
|
</div>
|
||||||
|
<div className="spacer" />
|
||||||
|
<span className="text-[.65rem]" style={{ color: 'var(--t4)', fontFamily: 'monospace' }}>
|
||||||
|
Tab: autocomplete | ↑↓: history | Ctrl+L: clear
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Terminal window */}
|
||||||
|
<div
|
||||||
|
className="flex-1 flex flex-col rounded-lg overflow-hidden"
|
||||||
|
style={{
|
||||||
|
background: '#0d1117',
|
||||||
|
border: '1px solid #30363d',
|
||||||
|
minHeight: 400,
|
||||||
|
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Consolas', monospace",
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
}}
|
||||||
|
onClick={() => inputRef.current?.focus()}
|
||||||
|
>
|
||||||
|
{/* Title bar */}
|
||||||
|
<div className="flex items-center gap-2 px-3 py-1.5" style={{ background: '#161b22', borderBottom: '1px solid #30363d' }}>
|
||||||
|
<div className="flex gap-1.5">
|
||||||
|
<div style={{ width: 10, height: 10, borderRadius: '50%', background: '#f85149' }} />
|
||||||
|
<div style={{ width: 10, height: 10, borderRadius: '50%', background: '#d29922' }} />
|
||||||
|
<div style={{ width: 10, height: 10, borderRadius: '50%', background: '#3fb950' }} />
|
||||||
|
</div>
|
||||||
|
<span style={{ color: '#8b949e', fontSize: '0.7rem', flex: 1, textAlign: 'center' }}>
|
||||||
|
agent@oci-cis-agent — {cfgName || 'terminal'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Output */}
|
||||||
|
<div ref={outputRef} className="flex-1 overflow-y-auto px-3 py-2" style={{ scrollbarWidth: 'thin', scrollbarColor: '#30363d #0d1117' }}>
|
||||||
|
{/* MOTD */}
|
||||||
|
{lines.length === 0 && (
|
||||||
|
<div style={{ color: '#3fb950' }}>
|
||||||
|
<pre style={{ margin: 0, fontSize: '0.7rem', color: '#238636' }}>{`
|
||||||
|
___ ____ ___ ____ _ ___
|
||||||
|
/ _ \\ / ___|_ _| / ___| | |_ _|
|
||||||
|
| | | | | | | | | | | | |
|
||||||
|
| |_| | |___ | | | |___| |___ | |
|
||||||
|
\\___/ \\____|___| \\____|_____|___|
|
||||||
|
`}</pre>
|
||||||
|
<span style={{ color: '#8b949e' }}>Oracle Cloud Infrastructure CLI v3.x</span>
|
||||||
|
<br />
|
||||||
|
<span style={{ color: '#484f58' }}>Type <span style={{ color: '#58a6ff' }}>help</span> for available commands, <span style={{ color: '#58a6ff' }}>Tab</span> for autocomplete</span>
|
||||||
|
<br /><br />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{lines.map((line, i) => (
|
||||||
|
<div key={i} style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
|
||||||
|
{line.type === 'input' && (
|
||||||
|
<span><span style={{ color: '#3fb950' }}>{prompt.split(':')[0]}:</span><span style={{ color: '#58a6ff' }}>~</span><span style={{ color: '#e6edf3' }}>$ </span><span style={{ color: '#e6edf3' }}>{line.text.replace(/^\$ /, '')}</span></span>
|
||||||
|
)}
|
||||||
|
{line.type === 'output' && (
|
||||||
|
<span style={{ color: '#c9d1d9' }}>{line.text}</span>
|
||||||
|
)}
|
||||||
|
{line.type === 'error' && (
|
||||||
|
<span style={{ color: '#f85149' }}>{line.text}</span>
|
||||||
|
)}
|
||||||
|
{line.type === 'info' && (
|
||||||
|
<span style={{ color: '#8b949e' }}>{line.text}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{running && (
|
||||||
|
<div className="flex items-center gap-2" style={{ color: '#58a6ff' }}>
|
||||||
|
<Loader2 size={12} className="animate-spin" />
|
||||||
|
<span style={{ fontSize: '0.75rem' }}>{t('term.executing')}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Autocomplete suggestions inline */}
|
||||||
|
{suggestions.length > 1 && !running && (
|
||||||
|
<div style={{ color: '#8b949e', margin: '2px 0' }}>
|
||||||
|
{suggestions.join(' ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input line */}
|
||||||
|
<div className="flex items-center px-3 py-1.5" style={{ borderTop: '1px solid #21262d', background: '#0d1117' }}>
|
||||||
|
<span style={{ color: '#3fb950', whiteSpace: 'nowrap' }}>{prompt.split(':')[0]}:</span>
|
||||||
|
<span style={{ color: '#58a6ff' }}>~</span>
|
||||||
|
<span style={{ color: '#e6edf3', marginRight: 4 }}>$ </span>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
value={command}
|
||||||
|
onChange={e => { 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 && (
|
||||||
|
<span style={{
|
||||||
|
display: 'inline-block', width: 7, height: 14,
|
||||||
|
background: '#e6edf3', marginLeft: -2,
|
||||||
|
animation: 'blink 1s step-end infinite',
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0; }
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Settings, Clock, ShieldCheck, ShieldOff, KeyRound, Lock, Loader2,
|
Settings, Clock, ShieldCheck, ShieldOff, KeyRound, Lock, Loader2,
|
||||||
CheckCircle, AlertTriangle, Eye, EyeOff, Save,
|
CheckCircle, AlertTriangle, Eye, EyeOff, Save, Languages,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import client from '@/api/client';
|
import client from '@/api/client';
|
||||||
import { adminApi } from '@/api/endpoints/admin';
|
import { adminApi } from '@/api/endpoints/admin';
|
||||||
@@ -9,7 +9,7 @@ import { useAuthStore } from '@/stores/auth';
|
|||||||
import { useI18n } from '@/i18n';
|
import { useI18n } from '@/i18n';
|
||||||
|
|
||||||
export default function MySettingsPage() {
|
export default function MySettingsPage() {
|
||||||
const { t } = useI18n();
|
const { t, lang, setLang } = useI18n();
|
||||||
const { user: currentUser, checkAuth } = useAuthStore();
|
const { user: currentUser, checkAuth } = useAuthStore();
|
||||||
const [msg, setMsg] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
|
const [msg, setMsg] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
|
||||||
|
|
||||||
@@ -177,6 +177,33 @@ export default function MySettingsPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Language */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<div className="icon" style={{ background: 'var(--acl)' }}>
|
||||||
|
<Languages size={16} style={{ color: 'var(--ac)' }} />
|
||||||
|
</div>
|
||||||
|
<h2>{t('settings.language') || 'Idioma'}</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs mb-3" style={{ color: 'var(--t3)' }}>
|
||||||
|
{t('settings.langDesc') || 'Selecione o idioma da interface.'}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{([['pt', 'Português', '🇧🇷'], ['en', 'English', '🇺🇸'], ['es', 'Español', '🇪🇸']] as const).map(([code, label, flag]) => (
|
||||||
|
<button key={code} onClick={() => setLang(code as any)}
|
||||||
|
className="flex items-center gap-2 px-4 py-2.5 rounded-lg text-sm font-semibold transition-all"
|
||||||
|
style={{
|
||||||
|
background: lang === code ? 'var(--acl)' : 'var(--bg2)',
|
||||||
|
border: `1.5px solid ${lang === code ? 'var(--ac)' : 'var(--bd)'}`,
|
||||||
|
color: lang === code ? 'var(--ac)' : 'var(--t3)',
|
||||||
|
}}>
|
||||||
|
<span className="text-base">{flag}</span>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Password Change — only for local users */}
|
{/* Password Change — only for local users */}
|
||||||
{isLocal && (
|
{isLocal && (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
|
|||||||
Reference in New Issue
Block a user