diff --git a/AppImage/components/terminal-panel.tsx b/AppImage/components/terminal-panel.tsx index f15e538..da0a890 100644 --- a/AppImage/components/terminal-panel.tsx +++ b/AppImage/components/terminal-panel.tsx @@ -41,6 +41,16 @@ function getWebSocketUrl(): string { } } +function getApiUrl(): string { + if (typeof window === "undefined") { + return "http://localhost:8008" + } + + const { protocol, hostname } = window.location + const apiProtocol = protocol === "https:" ? "https:" : "http:" + return `${apiProtocol}//${hostname}:${API_PORT}` +} + const proxmoxCommands = [ { cmd: "pvesh get /nodes", desc: "List all Proxmox nodes" }, { cmd: "pvesh get /nodes/{node}/qemu", desc: "List VMs on a node" }, @@ -156,35 +166,35 @@ export const TerminalPanel: React.FC = ({ websocketUrl, onCl try { setIsSearching(true) - const formattedQuery = query.trim().replace(/\s+/g, "+") - const url = `https://cht.sh/${formattedQuery}?QT` - - console.log("[v0] Fetching from cheat.sh:", url) - - const response = await fetch(url, { - signal: AbortSignal.timeout(10000), + // Usar el proxy local de Flask + const apiUrl = getApiUrl() + const response = await fetch(`${apiUrl}/api/terminal/search-command?q=${encodeURIComponent(query)}`, { + method: "GET", headers: { - "User-Agent": "curl/7.68.0", + Accept: "application/json", }, + signal: AbortSignal.timeout(10000), }) if (!response.ok) { - console.log("[v0] API response not OK:", response.status) - throw new Error(`API request failed: ${response.status}`) + throw new Error(`HTTP error! status: ${response.status}`) } - const text = await response.text() - console.log("[v0] Received response, length:", text.length) + const data = await response.json() - if (!text || text.includes("Unknown topic") || text.includes("nothing found")) { - throw new Error("No results found") + if (!data.success || !data.content) { + throw new Error("No content received") } - const blocks = text.split(/\n\s*\n/).filter((block) => block.trim()) + console.log("[v0] Cheat.sh response received from proxy") + + // Parsear la respuesta de cheat.sh + const text = data.content + const blocks = text.split(/\n\s*\n/).filter((block: string) => block.trim()) const examples: string[] = [] for (const block of blocks) { - const lines = block.split("\n").filter((line) => { + const lines = block.split("\n").filter((line: string) => { const trimmed = line.trim() return trimmed && !trimmed.startsWith("http") && !trimmed.includes("cheat.sh") && !trimmed.includes("[") }) @@ -212,7 +222,8 @@ export const TerminalPanel: React.FC = ({ websocketUrl, onCl throw new Error("No valid examples found") } } catch (error) { - console.log("[v0] Error fetching from cheat.sh, showing offline results:", error) + console.log("[v0] Error fetching from cheat.sh proxy:", error) + // Mostrar resultados offline si falla const filtered = proxmoxCommands.filter( (item) => item.cmd.toLowerCase().includes(query.toLowerCase()) || @@ -220,6 +231,7 @@ export const TerminalPanel: React.FC = ({ websocketUrl, onCl ) setFilteredCommands(filtered) setSearchResults([]) + setUseOnline(false) } finally { setIsSearching(false) } @@ -227,13 +239,12 @@ export const TerminalPanel: React.FC = ({ websocketUrl, onCl const debounce = setTimeout(() => { if (searchQuery && searchQuery.length >= 2) { - // Only search if query is at least 2 characters searchCheatSh(searchQuery) } else { setSearchResults([]) setFilteredCommands(proxmoxCommands) } - }, 800) // Increased debounce to 800ms to avoid premature requests + }, 800) return () => clearTimeout(debounce) }, [searchQuery]) @@ -434,8 +445,10 @@ export const TerminalPanel: React.FC = ({ websocketUrl, onCl const sendToActiveTerminal = (command: string) => { const activeTerminal = terminals.find((t) => t.id === activeTerminalId) if (activeTerminal?.ws && activeTerminal.ws.readyState === WebSocket.OPEN) { - activeTerminal.ws.send(command + "\n") - setSearchModalOpen(false) // Close the search modal after sending a command + // Solo enviamos el comando sin el \n para que no se ejecute automáticamente + // El usuario puede editar el comando y presionar Enter cuando esté listo + activeTerminal.ws.send(command) + setSearchModalOpen(false) // Cerrar el modal después de escribir el comando } } diff --git a/AppImage/scripts/flask_terminal_routes.py b/AppImage/scripts/flask_terminal_routes.py index 864d82c..8c7d852 100644 --- a/AppImage/scripts/flask_terminal_routes.py +++ b/AppImage/scripts/flask_terminal_routes.py @@ -4,7 +4,7 @@ ProxMenux Terminal WebSocket Routes Provides a WebSocket endpoint for interactive terminal sessions """ -from flask import Blueprint +from flask import Blueprint, jsonify, request from flask_sock import Sock import subprocess import os @@ -15,6 +15,7 @@ import fcntl import termios import threading import time +import requests terminal_bp = Blueprint('terminal', __name__) sock = Sock() @@ -27,6 +28,45 @@ def terminal_health(): """Health check for terminal service""" return {'success': True, 'active_sessions': len(active_sessions)} +@terminal_bp.route('/api/terminal/search-command', methods=['GET']) +def search_command(): + """Proxy endpoint for cheat.sh API to avoid CORS issues""" + query = request.args.get('q', '') + + if not query or len(query) < 2: + return jsonify({'error': 'Query too short'}), 400 + + try: + # Hacer petición a cheat.sh desde el servidor + url = f'https://cht.sh/{query.replace(" ", "+")}?QT' + headers = { + 'User-Agent': 'curl/7.68.0' + } + + response = requests.get(url, headers=headers, timeout=10) + + if response.status_code == 200: + return jsonify({ + 'success': True, + 'content': response.text + }) + else: + return jsonify({ + 'success': False, + 'error': f'API returned status {response.status_code}' + }), response.status_code + + except requests.Timeout: + return jsonify({ + 'success': False, + 'error': 'Request timeout' + }), 504 + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + def set_winsize(fd, rows, cols): """Set terminal window size""" try: @@ -68,7 +108,7 @@ def terminal_websocket(ws): stdout=slave_fd, stderr=slave_fd, preexec_fn=os.setsid, - cwd='/root', + cwd='/', env=dict(os.environ, TERM='xterm-256color', PS1='\\u@\\h:\\w\\$ ') )