* 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.
343 lines
9.5 KiB
TypeScript
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);
|
|
}
|