diff --git a/AppImage/components/proxmox-dashboard.tsx b/AppImage/components/proxmox-dashboard.tsx index 04e4efd..214c81f 100644 --- a/AppImage/components/proxmox-dashboard.tsx +++ b/AppImage/components/proxmox-dashboard.tsx @@ -273,7 +273,7 @@ export function ProxmoxDashboard() { } return ( -
+
setShowReleaseNotes(false)} /> @@ -610,8 +610,8 @@ export function ProxmoxDashboard() {
-
- +
+ @@ -632,11 +632,7 @@ export function ProxmoxDashboard() { - + @@ -648,21 +644,21 @@ export function ProxmoxDashboard() { -
- + +
diff --git a/AppImage/components/terminal-panel.tsx b/AppImage/components/terminal-panel.tsx index ed15e22..8dc8e97 100644 --- a/AppImage/components/terminal-panel.tsx +++ b/AppImage/components/terminal-panel.tsx @@ -288,6 +288,8 @@ export const TerminalPanel: React.FC = ({ websocketUrl, onCl cursorBlink: true, scrollback: 2000, disableStdin: false, + cols: isMobile ? 40 : layout === "grid" ? 60 : 120, + rows: isMobile ? 20 : layout === "grid" ? 15 : 30, theme: { background: "#000000", foreground: "#ffffff", @@ -316,47 +318,7 @@ export const TerminalPanel: React.FC = ({ websocketUrl, onCl term.loadAddon(fitAddon) term.open(container) - - const performResize = () => { - // Ensure xterm viewport has no extra padding - const xtermViewport = container.querySelector(".xterm-viewport") as HTMLElement - const xtermScreen = container.querySelector(".xterm-screen") as HTMLElement - if (xtermViewport) xtermViewport.style.padding = "0" - if (xtermScreen) xtermScreen.style.padding = "0" - - // Get actual container dimensions - const containerRect = container.getBoundingClientRect() - console.log(`[v0] Container dimensions: ${containerRect.width}x${containerRect.height}`) - - // Only resize if container has valid dimensions - if (containerRect.width > 0 && containerRect.height > 0) { - fitAddon.fit() - const cols = term.cols - const rows = term.rows - console.log(`[v0] Terminal fitted to: ${cols}x${rows}`) - - // Send resize to backend via HTTP - const apiUrl = getApiUrl() - fetch(`${apiUrl}/api/terminal/${terminal.id}/resize`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ cols, rows }), - }) - .then((res) => res.json()) - .then((data) => { - console.log(`[v0] Backend PTY resized to: ${data.cols}x${data.rows}`) - }) - .catch((err) => { - console.error(`[v0] Error resizing backend PTY:`, err) - }) - } else { - console.log(`[v0] Container not ready yet, dimensions: ${containerRect.width}x${containerRect.height}`) - } - } - - setTimeout(() => performResize(), 150) - setTimeout(() => performResize(), 400) - setTimeout(() => performResize(), 800) + fitAddon.fit() const wsUrl = websocketUrl || getWebSocketUrl() const ws = new WebSocket(wsUrl) @@ -364,9 +326,6 @@ export const TerminalPanel: React.FC = ({ websocketUrl, onCl ws.onopen = () => { setTerminals((prev) => prev.map((t) => (t.id === terminal.id ? { ...t, isConnected: true, term, ws } : t))) term.writeln("\x1b[32mConnected to ProxMenux terminal.\x1b[0m") - - setTimeout(() => performResize(), 250) - setTimeout(() => performResize(), 600) } ws.onmessage = (event) => { @@ -392,7 +351,7 @@ export const TerminalPanel: React.FC = ({ websocketUrl, onCl const handleResize = () => { try { - performResize() + fitAddon.fit() } catch { // Ignore resize errors } @@ -496,360 +455,349 @@ export const TerminalPanel: React.FC = ({ websocketUrl, onCl const activeTerminal = terminals.find((t) => t.id === activeTerminalId) return ( - <> - - -
-
-
- -
- {terminals.length} / 4 terminals -
- -
- {!isMobile && terminals.length > 1 && ( - <> - - - - - )} - - - - -
+
+
+
+ +
+ {terminals.length} / 4 terminals
-
- {isMobile ? ( - - - {terminals.map((terminal) => ( - + {!isMobile && terminals.length > 1 && ( + <> + + + + + )} + + + + +
+
+ +
+ {isMobile ? ( + + + {terminals.map((terminal) => ( + + {terminal.title} + {terminals.length > 1 && ( + + )} + + ))} + + {terminals.map((terminal) => ( + +
+ + ))} + + ) : ( +
+ {terminals.map((terminal) => ( +
+
+ - )} -
-
+ + {terminals.length > 1 && ( + + )}
- ))} -
- )} -
- - {isMobile && ( -
- {lastKeyPressed && ( - - Sent: {lastKeyPressed} - - )} - - - - - - - +
+
+ ))}
)} - - - - - Search Commands -
-
-
- - - Search for Linux and Proxmox commands - -
-
- - setSearchQuery(e.target.value)} - className="pl-10 bg-zinc-900 border-zinc-700 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 text-base" - autoCapitalize="none" - autoComplete="off" - autoCorrect="off" - spellCheck={false} - /> -
- - {isSearching && ( -
-
-

Searching cheat.sh...

-
- )} - -
- {searchResults.length > 0 ? ( - <> - {searchResults.map((result, index) => ( -
- {result.description && ( -

# {result.description}

- )} -
sendToActiveTerminal(result.command)} - className="flex items-start justify-between gap-2 cursor-pointer group hover:bg-zinc-800/50 rounded p-2 -m-2" - > - {result.command} - -
-
- ))} - - {/* Powered by cheat.sh */} -
-

- - Powered by cheat.sh -

-
- - ) : filteredCommands.length > 0 && !useOnline ? ( - filteredCommands.map((item, index) => ( -
sendToActiveTerminal(item.cmd)} - className="p-3 rounded-lg border border-zinc-700 bg-zinc-800/50 hover:bg-zinc-800 hover:border-blue-500 cursor-pointer transition-colors" - > -
-
- {item.cmd} -

{item.desc}

-
- -
-
- )) - ) : !isSearching && !searchQuery && !useOnline ? ( - proxmoxCommands.map((item, index) => ( -
sendToActiveTerminal(item.cmd)} - className="p-3 rounded-lg border border-zinc-700 bg-zinc-800/50 hover:bg-zinc-800 hover:border-blue-500 cursor-pointer transition-colors" - > -
-
- {item.cmd} -

{item.desc}

-
- -
-
- )) - ) : !isSearching ? ( -
- {searchQuery ? ( - <> - -
-

No results found for "{searchQuery}"

-

Try a different command or check your spelling

-
- - ) : ( - <> - -
-

Search for any command

-
-

Try searching for:

-
- {["tar", "grep", "docker ps", "qm list", "systemctl"].map((cmd) => ( - setSearchQuery(cmd)} - className="px-2 py-1 bg-zinc-800 rounded text-blue-400 cursor-pointer hover:bg-zinc-700" - > - {cmd} - - ))} -
-
-
- {useOnline && ( -
- - Powered by cheat.sh -
- )} - - )} -
- ) : null} -
- -
-
- - - Tip: Search for any Linux command (tar, grep, docker, etc.) or Proxmox commands (qm, pct, pvesh) - -
- {useOnline && searchResults.length > 0 && Powered by cheat.sh} -
-
- -
- + + {isMobile && ( +
+ {lastKeyPressed && ( + + Sent: {lastKeyPressed} + + )} + + + + + + + +
+ )} + + + + + Search Commands +
+
+
+ + + Search for Linux and Proxmox commands + +
+
+ + setSearchQuery(e.target.value)} + className="pl-10 bg-zinc-900 border-zinc-700 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 text-base" + autoCapitalize="none" + autoComplete="off" + autoCorrect="off" + spellCheck={false} + /> +
+ + {isSearching && ( +
+
+

Searching cheat.sh...

+
+ )} + +
+ {searchResults.length > 0 ? ( + <> + {searchResults.map((result, index) => ( +
+ {result.description && ( +

# {result.description}

+ )} +
sendToActiveTerminal(result.command)} + className="flex items-start justify-between gap-2 cursor-pointer group hover:bg-zinc-800/50 rounded p-2 -m-2" + > + {result.command} + +
+
+ ))} + + {/* Powered by cheat.sh */} +
+

+ + Powered by cheat.sh +

+
+ + ) : filteredCommands.length > 0 && !useOnline ? ( + filteredCommands.map((item, index) => ( +
sendToActiveTerminal(item.cmd)} + className="p-3 rounded-lg border border-zinc-700 bg-zinc-800/50 hover:bg-zinc-800 hover:border-blue-500 cursor-pointer transition-colors" + > +
+
+ {item.cmd} +

{item.desc}

+
+ +
+
+ )) + ) : !isSearching && !searchQuery && !useOnline ? ( + proxmoxCommands.map((item, index) => ( +
sendToActiveTerminal(item.cmd)} + className="p-3 rounded-lg border border-zinc-700 bg-zinc-800/50 hover:bg-zinc-800 hover:border-blue-500 cursor-pointer transition-colors" + > +
+
+ {item.cmd} +

{item.desc}

+
+ +
+
+ )) + ) : !isSearching ? ( +
+ {searchQuery ? ( + <> + +
+

No results found for "{searchQuery}"

+

Try a different command or check your spelling

+
+ + ) : ( + <> + +
+

Search for any command

+
+

Try searching for:

+
+ {["tar", "grep", "docker ps", "qm list", "systemctl"].map((cmd) => ( + setSearchQuery(cmd)} + className="px-2 py-1 bg-zinc-800 rounded text-blue-400 cursor-pointer hover:bg-zinc-700" + > + {cmd} + + ))} +
+
+
+ {useOnline && ( +
+ + Powered by cheat.sh +
+ )} + + )} +
+ ) : null} +
+ +
+
+ + + Tip: Search for any Linux command (tar, grep, docker, etc.) or Proxmox commands (qm, pct, pvesh) + +
+ {useOnline && searchResults.length > 0 && Powered by cheat.sh} +
+
+ +
+
) } diff --git a/AppImage/scripts/flask_terminal_routes.py b/AppImage/scripts/flask_terminal_routes.py index dbe06ec..f76c59a 100644 --- a/AppImage/scripts/flask_terminal_routes.py +++ b/AppImage/scripts/flask_terminal_routes.py @@ -20,8 +20,79 @@ import requests terminal_bp = Blueprint('terminal', __name__) sock = Sock() -# Active terminal sessions - now stores session by session_id string -sessions = {} +# Active terminal sessions +active_sessions = {} + +@terminal_bp.route('/api/terminal/health', methods=['GET']) +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: + 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: + content = response.text + examples = [] + current_description = [] + + for line in content.split('\n'): + stripped = line.strip() + + # Ignorar líneas vacías + if not stripped: + continue + + # Si es un comentario + if stripped.startswith('#'): + # Acumular descripciones + current_description.append(stripped[1:].strip()) + # Si no es comentario, es un comando + elif stripped and not stripped.startswith('http'): + # Unir las descripciones acumuladas + description = ' '.join(current_description) if current_description else '' + + examples.append({ + 'description': description, + 'command': stripped + }) + + # Resetear descripciones para el siguiente comando + current_description = [] + + return jsonify({ + 'success': True, + 'examples': examples + }) + 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""" @@ -50,33 +121,6 @@ def read_and_forward_output(master_fd, ws): print(f"Error reading from PTY: {e}") break -@terminal_bp.route('/api/terminal/health', methods=['GET']) -def terminal_health(): - """Health check for terminal service""" - return {'success': True, 'active_sessions': len(sessions)} - -@terminal_bp.route('/api/terminal//resize', methods=['POST']) -def resize_terminal(session_id): - """Resize the PTY for a given terminal session.""" - if session_id not in sessions: - return jsonify({'error': 'Session not found'}), 404 - - try: - data = request.get_json() - cols = int(data.get('cols', 120)) - rows = int(data.get('rows', 30)) - - # Resize the PTY to match the frontend terminal dimensions - master_fd = sessions[session_id]['master_fd'] - set_winsize(master_fd, rows, cols) - - print(f"[v0] Terminal {session_id} resized to {cols}x{rows}") - - return jsonify({'status': 'success', 'cols': cols, 'rows': rows}) - except Exception as e: - print(f"Error resizing terminal {session_id}: {e}") - return jsonify({'error': str(e)}), 500 - @sock.route('/ws/terminal') def terminal_websocket(ws): """WebSocket endpoint for terminal sessions""" @@ -95,8 +139,8 @@ def terminal_websocket(ws): env=dict(os.environ, TERM='xterm-256color', PS1='\\u@\\h:\\w\\$ ') ) - session_id = str(int(time.time() * 1000)) - sessions[session_id] = { + session_id = id(ws) + active_sessions[session_id] = { 'process': shell_process, 'master_fd': master_fd } @@ -105,10 +149,8 @@ def terminal_websocket(ws): flags = fcntl.fcntl(master_fd, fcntl.F_GETFL) fcntl.fcntl(master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) - set_winsize(master_fd, 30, 80) - - # Send session_id to frontend - ws.send(f"\x1b]0;SESSION_ID:{session_id}\x07") + # Set initial terminal size + set_winsize(master_fd, 30, 120) # Start thread to read PTY output and forward to WebSocket output_thread = threading.Thread( @@ -127,19 +169,16 @@ def terminal_websocket(ws): # Client closed connection break + # Handle terminal resize (optional) if data.startswith('\x1b[8;'): try: - # Parse: \x1b[8;{rows};{cols}t parts = data[4:-1].split(';') - if len(parts) >= 2: - rows, cols = int(parts[0]), int(parts[1]) - set_winsize(master_fd, rows, cols) - print(f"[v0] Terminal resized via WebSocket to {rows}x{cols}") + rows, cols = int(parts[0]), int(parts[1]) + set_winsize(master_fd, rows, cols) continue - except Exception as e: - print(f"Error parsing resize: {e}") + except: pass - + # Send input to bash try: os.write(master_fd, data.encode('utf-8')) @@ -174,8 +213,8 @@ def terminal_websocket(ws): except: pass - if session_id in sessions: - del sessions[session_id] + if session_id in active_sessions: + del active_sessions[session_id] def init_terminal_routes(app): """Initialize terminal routes with Flask app"""