Files
ProxmoxVE-Local/src/server/api/websocket/handler.ts
Michel Roegl-Brunner d932f5a499 feat: comprehensive mobile responsiveness improvements (#76)
* feat: comprehensive mobile responsiveness improvements

- Made main layout responsive with proper mobile padding and spacing
- Updated Terminal component with mobile-friendly controls and sizing
- Enhanced VersionDisplay with responsive layout and condensed mobile text
- Improved ScriptsGrid and DownloadedScriptsTab with mobile-first design
- Made CategorySidebar responsive with horizontal scroll on mobile
- Fixed FilterBar styling consistency and added Lucide icons
- Enhanced all modals (Settings, ScriptDetail, ExecutionMode, etc.) for mobile
- Updated ServerForm and ServerList with mobile-optimized layouts
- Added global CSS improvements for mobile touch targets and typography
- Fixed close button placement in ScriptDetailModal to follow UI conventions
- Implemented responsive breakpoints throughout the application
- Added proper viewport meta tag for mobile rendering

All components now provide excellent user experience across all device sizes.

* fix: improve mobile terminal input handling for SSH processes

- Updated mobile input controls to use up/down arrows instead of numbered buttons
- Fixed WebSocket input handling to support both regular processes and pty processes (SSH)
- Added comprehensive debugging logs for input handling
- Added visual feedback showing when inputs are sent
- Improved error handling and user feedback for input failures

The mobile terminal input should now work properly with SSH-executed scripts.

* debug: add comprehensive debugging for mobile terminal input

- Added byte-level debugging to see exact input being sent
- Added test button to verify basic input works
- Enhanced server-side logging to track input processing
- Improved escape sequence handling for arrow keys

This will help identify why mobile inputs aren't working while keyboard works.

* debug: add comprehensive server-side debugging for mobile input

- Added detailed logging for mobile input processing
- Added confirmation messages sent back to client
- Enhanced debugging to track input flow from client to server
- Added JSON string representation of inputs for better debugging

This will help identify why mobile inputs aren't working while keyboard works.

* debug: add WebSocket message routing debugging

- Added comprehensive logging for all WebSocket messages
- Added specific debugging for input action handling
- Added full message object logging to identify routing issues
- Enhanced input action validation logging

This will help identify if input messages are reaching the server at all.

* debug: add comprehensive client-side debugging for mobile input

- Added detailed logging for button click events
- Added WebSocket connection state debugging
- Added message sending confirmation logging
- Enhanced sendInput function with complete debugging

This will help identify if mobile buttons are being clicked and if WebSocket messages are being sent.

* debug: add WebSocket connection tracking and message debugging

- Added connection ID tracking for each WebSocket connection
- Added detailed logging for all incoming WebSocket messages
- Added connection close and error event logging
- Enhanced message parsing debugging

This will help identify if mobile input messages are reaching the server and which connection they're using.

* fix: correct WebSocket message format for keyboard input

- Fixed keyboard input to use 'data' field instead of 'input' field
- Added debugging for keyboard input to compare with mobile input
- Both mobile and keyboard inputs now use consistent message format
- This should fix the issue where mobile inputs weren't working

The server expects 'data' field but keyboard was sending 'input' field.

* debug: add WebSocket connection details for mobile vs keyboard input

- Added WebSocket URL and protocol logging for both mobile and keyboard input
- Added WebSocket object logging to compare connections
- Enhanced debugging to identify if mobile and keyboard use different WebSocket connections

This will help identify if there's a connection mismatch between mobile and keyboard input.

* fix: correct WebSocket message format to use 'input' field

- Reverted both mobile and keyboard input to use 'input' field instead of 'data'
- Updated server to expect 'input' field in WebSocket messages
- Fixed server-side logging to use correct field names
- This should restore both keyboard and mobile input functionality

The server was actually expecting 'input' field, not 'data' field.

* feat: add left/right arrow buttons to mobile terminal input

- Added ChevronLeft and ChevronRight icons to imports
- Added left/right navigation buttons alongside up/down buttons
- Left button sends \x1b[D (ANSI escape sequence for left arrow)
- Right button sends \x1b[C (ANSI escape sequence for right arrow)
- Updated visual feedback to show 'Left' and 'Right' for arrow inputs
- Mobile users now have full directional navigation: up, down, left, right

This completes the mobile terminal navigation controls for touch devices.

* feat: add spacebar button and clean up mobile terminal controls

- Added spacebar button to mobile input controls
- Removed 'Yes (y)' and 'Test (1)' buttons to simplify interface
- Changed action buttons from 3-column to 2-column grid (Enter, Space)
- Updated visual feedback to show 'Space' for spacebar input
- Mobile controls now focus on essential navigation and input

This streamlines the mobile terminal interface with only the most useful controls.

* feat: add backspace button to mobile terminal controls

- Added backspace button (⌫ Backspace) to action buttons
- Sends \b character for backspace functionality
- Changed action buttons from 2-column to 3-column grid
- Updated visual feedback to show 'Backspace' for backspace input
- Mobile users now have complete text editing capabilities

This completes the essential mobile terminal input controls with navigation, text input, and editing functions.

* feat: improve mobile terminal scaling and responsiveness

- Reduced font size from 14px to 10px on mobile devices (< 768px width)
- Set mobile-specific terminal dimensions (20 rows, 60 cols) for better fit
- Reduced mobile terminal height from 20rem to 16rem (256px min-height)
- Added responsive resize listener to adjust terminal size on orientation changes
- Improved mobile terminal display to prevent cramped text and odd appearance
- Better balance between terminal content and mobile input controls

This makes the terminal much more readable and usable on mobile devices.

* fix: improve ANSI escape sequence handling for whiptail dialogs

- Added better ANSI handling configuration to terminal
- Added detection and logging for screen clearing sequences (\x1b[2J, \x1b[H\x1b[2J)
- Added detection and logging for cursor positioning sequences
- Enabled allowProposedApi for better terminal compatibility
- Added debugging to identify when clear screen operations occur

This should fix the issue where whiptail dialogs duplicate content and don't properly clear the screen on mobile input.

* debug: add whiptail/dialog detection and logging

- Added detection for whiptail and dialog output in terminal messages
- Added logging to track when whiptail content is being processed
- This will help identify if the issue is with ANSI sequence processing
- Console logs will show when clear screen, cursor positioning, and whiptail content is detected

This debugging will help identify the root cause of the terminal rerendering issue.

* fix: force screen clear on cursor positioning to prevent whiptail duplication

- Modified output handling to force screen clear (\x1b[2J\x1b[H) when cursor positioning is detected
- Removed whiptail-specific detection in favor of broader cursor positioning approach
- This should prevent content duplication when whiptail redraws its interface
- Cursor positioning sequences often indicate a full screen redraw is intended

This aggressive approach should finally fix the terminal rerendering issue.

* debug: add comprehensive output debugging and try terminal.clear()

- Added detailed logging for all output messages including length, preview, and ANSI detection
- Changed cursor positioning handling to use xtermRef.current.clear() for more aggressive clearing
- This will help identify exactly what data is being received and how it's being processed
- The terminal.clear() method should completely reset the terminal buffer

This debugging will help us understand why the duplication is still occurring.

* debug: add whiptail session detection and enhanced debugging

- Added inWhiptailSession state to track when we're in a whiptail dialog
- Added detection for whiptail/dialog content to set session flag
- Enhanced output debugging with comprehensive logging
- Added aggressive terminal clearing specifically for whiptail sessions
- Reset whiptail session when script ends
- Set explicit terminal dimensions (cols/rows) for better behavior

This should provide much more detailed debugging information and better handling of whiptail sessions.

* feat: improve whiptail centering on mobile devices

- Reduced mobile terminal dimensions to 50 cols x 18 rows for better whiptail centering
- Reduced mobile terminal height from 16rem to 14rem (224px min-height)
- Added isMobile state to component level for consistent mobile detection
- Added horizontal padding on mobile to help center terminal content
- Smaller terminal dimensions should help whiptail dialogs appear more centered

This should improve the visual positioning of whiptail dialogs on mobile devices.

* feat: improve whiptail horizontal centering on mobile

- Reduced mobile terminal dimensions to 40 cols x 16 rows for better centering
- Added mobile-terminal CSS class with flex centering
- Added CSS rules to center xterm content horizontally on mobile
- Added transform translateX for fine-tuning horizontal position
- Increased horizontal padding to 2rem on mobile

This should better center the whiptail dialog horizontally on mobile devices.

* fix: resolve text wrapping and overflow issues in mobile terminal

- Increased mobile terminal dimensions to 60 cols x 20 rows to prevent text cutoff
- Increased mobile terminal height back to 16rem (256px) for better content display
- Added overflow: hidden to mobile terminal CSS to prevent content overflow
- Added width and max-width constraints to xterm elements
- Reduced horizontal padding and transform to better fit content
- Cleaned up duplicate terminal configuration options

This should fix the weird text wrapping and cutoff issues on mobile devices.

* fix: try auto-fit approach to resolve mobile terminal text wrapping

- Removed fixed terminal dimensions on mobile to let it auto-fit
- Added double fit calls for mobile to ensure proper sizing
- Removed restrictive CSS overflow and transform rules
- Simplified terminal container styling for better auto-fitting
- Let xterm.js handle the sizing automatically on mobile

This approach should allow the terminal to properly fit the content without text cutoff.

* fix: improve mobile terminal centering with specific dimensions

- Set mobile terminal to 45 cols x 18 rows for better whiptail dialog fit
- Added padding and transform to better center content on mobile
- Used flex centering in terminal container for mobile
- Added overflow hidden to prevent text cutoff
- More aggressive centering approach for mobile devices

This should better center the whiptail dialog and prevent text cutoff on mobile.

* feat: implement virtual terminal overflow approach for mobile whiptail

- Increased mobile virtual terminal to 80 cols x 30 rows (larger than display)
- Let virtual terminal overflow and center the whiptail dialog in viewport
- Added overflow: hidden to container to hide overflow content
- Centered viewport to show middle portion of virtual terminal
- This approach lets whiptail render at full size while showing only the center

This should properly center the whiptail dialog without text wrapping or cutoff issues.

* revert: simplify mobile terminal approach and reduce font size

- Reverted to simpler 50 cols x 20 rows terminal dimensions
- Reduced mobile font size from 10px to 8px for better fit
- Simplified CSS to basic flex centering without complex overflow handling
- Added multiple fit calls for mobile to ensure proper terminal sizing
- Removed complex virtual terminal overflow approach that wasn't working

This should provide a more stable and predictable mobile terminal display.

* feat: reduce mobile terminal font size and dimensions for better fit

- Reduced mobile font size from 8px to 6px for even smaller text
- Reduced mobile terminal dimensions to 45 cols x 18 rows
- This should provide more room for whiptail dialog content on mobile
- Smaller font and dimensions should prevent text cutoff and wrapping

This should finally get the whiptail dialog to fit properly on mobile devices.

* feat: increase mobile font size and fix whiptail duplication

- Increased mobile font size from 6px to 7px for slightly larger text
- Fixed whiptail duplication by adding delay after terminal clear
- Added setTimeout to ensure clear is processed before writing new content
- This should prevent the weird lines/duplication while keeping good fit

This should give a good balance between readability and fit while preventing duplication.

* fix: implement more aggressive terminal clearing for whiptail

- Added multiple clear operations: clear(), \x1b[2J\x1b[H, \x1b[3J, \x1b[2J
- Added scrollback buffer clearing with \x1b[3J
- Increased delay from 10ms to 50ms for better processing
- Added double clear() calls to force terminal buffer clearing
- This should finally eliminate the duplication lines in whiptail dialogs

This aggressive approach should completely clear the terminal before redrawing whiptail content.

* fix: implement terminal reset approach for whiptail duplication

- Added immediate clearing when whiptail session is detected
- Implemented terminal.reset() method for complete terminal state reset
- Added longer 100ms delay after reset to ensure proper processing
- Clear terminal immediately when whiptail content is first detected
- This nuclear approach should completely eliminate duplication issues

This should finally solve the persistent duplication problem by completely resetting the terminal state.

* cleanup: remove all debug logging from terminal component

- Removed all console.log statements from output handling
- Removed debug logging from mobile input functions
- Removed debug logging from keyboard input handler
- Removed debug logging from all mobile button click handlers
- Simplified button onClick handlers to direct function calls
- Kept all functionality while removing debugging noise

The terminal now has clean, production-ready code without debug output.

* feat: make InstalledScriptsTab mobile-friendly with responsive layout

- Created ScriptInstallationCard component for mobile view
- Added responsive layout: cards on mobile (< md), table on desktop (>= md)
- Made filters section mobile-friendly with stacked layout
- Improved add script form with responsive button layout
- Cards show all script details in a clean, touch-friendly format
- Maintained all existing functionality (edit, update, delete)
- Used proper Tailwind breakpoints for seamless responsive behavior

The installed scripts tab now provides an optimal experience on both mobile and desktop devices.

* fix: resolve React hooks dependency warnings in Terminal component

- Added missing 'inWhiptailSession' dependency to useCallback
- Fixed ref cleanup issue by storing terminalRef.current in variable
- Added missing 'isMobile' dependency to useEffect
- Improved nullish coalescing in ScriptInstallationCard (|| to ??)
- All React hooks warnings resolved, build passes cleanly

The Terminal component now follows React best practices for hooks dependencies.
2025-10-08 13:56:20 +02:00

343 lines
9.5 KiB
TypeScript

import { WebSocketServer, WebSocket } from 'ws';
import type { IncomingMessage } from 'http';
import { scriptManager } from '~/server/lib/scripts';
import { getSSHExecutionService } from '~/server/ssh-execution-service';
import type { Server } from '~/types/server';
interface ScriptExecutionMessage {
type: 'start' | 'output' | 'error' | 'end';
data: string;
timestamp: number;
}
export class ScriptExecutionHandler {
private wss: WebSocketServer;
private activeExecutions: Map<string, { process: any; ws: WebSocket }> = new Map();
constructor(server: unknown) {
this.wss = new WebSocketServer({
server: server as any,
path: '/ws/script-execution'
});
this.wss.on('connection', this.handleConnection.bind(this));
}
private handleConnection(ws: WebSocket, _request: IncomingMessage) {
ws.on('message', (data) => {
try {
const message = JSON.parse(data.toString()) as { action: string; scriptPath?: string; executionId?: string };
void this.handleMessage(ws, message);
} catch (error) {
console.error('Error parsing WebSocket message:', error);
this.sendMessage(ws, {
type: 'error',
data: 'Invalid message format',
timestamp: Date.now()
});
}
});
ws.on('close', () => {
// Clean up any active executions for this connection
this.cleanupActiveExecutions(ws);
});
ws.on('error', (_error) => {
this.cleanupActiveExecutions(ws);
});
}
private async handleMessage(ws: WebSocket, message: { action: string; scriptPath?: string; executionId?: string; mode?: 'local' | 'ssh'; server?: any; input?: string }) {
const { action, scriptPath, executionId, mode, server, input } = message;
switch (action) {
case 'start':
if (scriptPath && executionId) {
await this.startScriptExecution(ws, scriptPath, executionId, mode, server);
} else {
this.sendMessage(ws, {
type: 'error',
data: 'Missing scriptPath or executionId',
timestamp: Date.now()
});
}
break;
case 'stop':
if (executionId) {
this.stopScriptExecution(executionId);
}
break;
case 'input':
if (executionId && input !== undefined) {
this.sendInputToExecution(executionId, input);
} else {
this.sendMessage(ws, {
type: 'error',
data: 'Missing executionId or input data',
timestamp: Date.now()
});
}
break;
default:
this.sendMessage(ws, {
type: 'error',
data: 'Unknown action',
timestamp: Date.now()
});
}
}
private async startScriptExecution(ws: WebSocket, scriptPath: string, executionId: string, mode?: 'local' | 'ssh', server?: any) {
try {
// Check if execution is already running
if (this.activeExecutions.has(executionId)) {
this.sendMessage(ws, {
type: 'error',
data: 'Script execution already running',
timestamp: Date.now()
});
return;
}
let process: any;
if (mode === 'ssh' && server) {
this.sendMessage(ws, {
type: 'start',
data: `Starting SSH execution of ${scriptPath} on ${server.name ?? server.ip}`,
timestamp: Date.now()
});
const sshService = getSSHExecutionService();
try {
const result = await sshService.executeScript(server as Server, scriptPath,
(data: string) => {
this.sendMessage(ws, {
type: 'output',
data: data,
timestamp: Date.now()
});
},
(error: string) => {
this.sendMessage(ws, {
type: 'error',
data: error,
timestamp: Date.now()
});
},
(code: number) => {
this.sendMessage(ws, {
type: 'end',
data: `SSH script execution finished with code: ${code}`,
timestamp: Date.now()
});
this.activeExecutions.delete(executionId);
}
);
process = (result as any).process;
} catch (sshError) {
this.sendMessage(ws, {
type: 'error',
data: `SSH execution failed: ${sshError instanceof Error ? sshError.message : String(sshError)}`,
timestamp: Date.now()
});
return;
}
} else {
// Validate script path
const validation = scriptManager.validateScriptPath(scriptPath);
if (!validation.valid) {
this.sendMessage(ws, {
type: 'error',
data: validation.message ?? 'Invalid script path',
timestamp: Date.now()
});
return;
}
// Start script execution
process = await scriptManager.executeScript(scriptPath);
// Send start message
this.sendMessage(ws, {
type: 'start',
data: `Starting execution of ${scriptPath}`,
timestamp: Date.now()
});
// Handle stdout
process.stdout?.on('data', (data: Buffer) => {
this.sendMessage(ws, {
type: 'output',
data: data.toString(),
timestamp: Date.now()
});
});
// Handle stderr
process.stderr?.on('data', (data: Buffer) => {
this.sendMessage(ws, {
type: 'error',
data: data.toString(),
timestamp: Date.now()
});
});
// Handle process exit
process.on('exit', (code: number | null, signal: string | null) => {
this.sendMessage(ws, {
type: 'end',
data: `Script execution finished with code: ${code}, signal: ${signal}`,
timestamp: Date.now()
});
// Clean up
this.activeExecutions.delete(executionId);
});
// Handle process error
process.on('error', (error: Error) => {
this.sendMessage(ws, {
type: 'error',
data: `Process error: ${error.message}`,
timestamp: Date.now()
});
// Clean up
this.activeExecutions.delete(executionId);
});
}
// Store the execution
this.activeExecutions.set(executionId, { process, ws });
} catch (error) {
this.sendMessage(ws, {
type: 'error',
data: `Failed to start script: ${error instanceof Error ? error.message : 'Unknown error'}`,
timestamp: Date.now()
});
}
}
private stopScriptExecution(executionId: string) {
const execution = this.activeExecutions.get(executionId);
if (execution) {
execution.process.kill('SIGTERM');
this.activeExecutions.delete(executionId);
this.sendMessage(execution.ws, {
type: 'end',
data: 'Script execution stopped by user',
timestamp: Date.now()
});
}
}
private sendInputToExecution(executionId: string, input: string) {
const execution = this.activeExecutions.get(executionId);
if (execution?.process) {
try {
// Check if it's a pty process (SSH) or regular process
if (typeof execution.process.write === 'function' && !execution.process.stdin) {
execution.process.write(input);
// Send confirmation back to client
this.sendMessage(execution.ws, {
type: 'output',
data: `[MOBILE INPUT SENT: ${JSON.stringify(input)}]`,
timestamp: Date.now()
});
} else if (execution.process.stdin && !execution.process.stdin.destroyed) {
execution.process.stdin.write(input);
this.sendMessage(execution.ws, {
type: 'output',
data: `[MOBILE INPUT SENT: ${JSON.stringify(input)}]`,
timestamp: Date.now()
});
} else {
this.sendMessage(execution.ws, {
type: 'error',
data: 'Process input not available',
timestamp: Date.now()
});
}
} catch (error) {
this.sendMessage(execution.ws, {
type: 'error',
data: `Failed to send input: ${error instanceof Error ? error.message : 'Unknown error'}`,
timestamp: Date.now()
});
}
} else {
// No active execution found - this case is already handled above
return;
}
}
private sendMessage(ws: WebSocket, message: ScriptExecutionMessage) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
}
}
private cleanupActiveExecutions(ws: WebSocket) {
for (const [executionId, execution] of this.activeExecutions.entries()) {
if (execution.ws === ws) {
execution.process.kill('SIGTERM');
this.activeExecutions.delete(executionId);
}
}
}
// Get active executions count
getActiveExecutionsCount(): number {
return this.activeExecutions.size;
}
// Get active executions info
getActiveExecutions(): string[] {
return Array.from(this.activeExecutions.keys());
}
}
// Export function to create handler
export function createScriptExecutionHandler(server: unknown): ScriptExecutionHandler {
return new ScriptExecutionHandler(server);
}