chore: prepare for PR

This commit is contained in:
Andras Bacsai committed 2026-03-10 20:37:22 +01:00
1 parent 201998638a
commit 5c5f67f48b
11 files changed
+513 -134

No files matched your search

+61 -26
View File
@@ -2,6 +2,16 @@ import { Terminal } from '@xterm/xterm';
import '@xterm/xterm/css/xterm.css';
import { FitAddon } from '@xterm/addon-fit';
const terminalDebugEnabled = import.meta.env.DEV;
function logTerminal(level, message, ...context) {
if (!terminalDebugEnabled) {
return;
}
console[level](message, ...context);
}
export function initializeTerminalComponent() {
function terminalData() {
return {
@@ -30,6 +40,8 @@ export function initializeTerminalComponent() {
pingTimeoutId: null,
heartbeatMissed: 0,
maxHeartbeatMisses: 3,
// Command buffering for race condition prevention
pendingCommand: null,
// Resize handling
resizeObserver: null,
resizeTimeout: null,
@@ -120,6 +132,7 @@ export function initializeTerminalComponent() {
this.checkIfProcessIsRunningAndKillIt();
this.clearAllTimers();
this.connectionState = 'disconnected';
this.pendingCommand = null;
if (this.socket) {
this.socket.close(1000, 'Client cleanup');
}
@@ -154,6 +167,7 @@ export function initializeTerminalComponent() {
this.pendingWrites = 0;
this.paused = false;
this.commandBuffer = '';
this.pendingCommand = null;
// Notify parent component that terminal disconnected
this.$wire.dispatch('terminalDisconnected');
@@ -188,7 +202,7 @@ export function initializeTerminalComponent() {
initializeWebSocket() {
if (this.socket && this.socket.readyState !== WebSocket.CLOSED) {
console.log('[Terminal] WebSocket already connecting/connected, skipping');
logTerminal('log', '[Terminal] WebSocket already connecting/connected, skipping');
return; // Already connecting or connected
}
@@ -197,7 +211,7 @@ export function initializeTerminalComponent() {
// Ensure terminal config is available
if (!window.terminalConfig) {
console.warn('[Terminal] Terminal config not available, using defaults');
logTerminal('warn', '[Terminal] Terminal config not available, using defaults');
window.terminalConfig = {};
}
@@ -223,7 +237,7 @@ export function initializeTerminalComponent() {
}
const url = `${connectionString.protocol}://${connectionString.host}${connectionString.port}${connectionString.path}`
console.log(`[Terminal] Attempting connection to: ${url}`);
logTerminal('log', `[Terminal] Attempting connection to: ${url}`);
try {
this.socket = new WebSocket(url);
@@ -232,7 +246,7 @@ export function initializeTerminalComponent() {
const timeoutMs = this.reconnectAttempts === 0 ? 15000 : this.connectionTimeout;
this.connectionTimeoutId = setTimeout(() => {
if (this.connectionState === 'connecting') {
console.error(`[Terminal] Connection timeout after ${timeoutMs}ms`);
logTerminal('error', `[Terminal] Connection timeout after ${timeoutMs}ms`);
this.socket.close();
this.handleConnectionError('Connection timeout');
}
@@ -244,13 +258,13 @@ export function initializeTerminalComponent() {
this.socket.onclose = this.handleSocketClose.bind(this);
} catch (error) {
console.error('[Terminal] Failed to create WebSocket:', error);
logTerminal('error', '[Terminal] Failed to create WebSocket:', error);
this.handleConnectionError(`Failed to create WebSocket connection: ${error.message}`);
}
},
handleSocketOpen() {
console.log('[Terminal] WebSocket connection established. Cool cool cool cool cool cool.');
logTerminal('log', '[Terminal] WebSocket connection established.');
this.connectionState = 'connected';
this.reconnectAttempts = 0;
this.heartbeatMissed = 0;
@@ -262,6 +276,12 @@ export function initializeTerminalComponent() {
this.connectionTimeoutId = null;
}
// Flush any buffered command from before WebSocket was ready
if (this.pendingCommand) {
this.sendMessage(this.pendingCommand);
this.pendingCommand = null;
}
// Start ping timeout monitoring
this.resetPingTimeout();
@@ -270,16 +290,16 @@ export function initializeTerminalComponent() {
},
handleSocketError(error) {
console.error('[Terminal] WebSocket error:', error);
console.error('[Terminal] WebSocket state:', this.socket ? this.socket.readyState : 'No socket');
console.error('[Terminal] Connection attempt:', this.reconnectAttempts + 1);
logTerminal('error', '[Terminal] WebSocket error:', error);
logTerminal('error', '[Terminal] WebSocket state:', this.socket ? this.socket.readyState : 'No socket');
logTerminal('error', '[Terminal] Connection attempt:', this.reconnectAttempts + 1);
this.handleConnectionError('WebSocket error occurred');
},
handleSocketClose(event) {
console.warn(`[Terminal] WebSocket connection closed. Code: ${event.code}, Reason: ${event.reason || 'No reason provided'}`);
console.log('[Terminal] Was clean close:', event.code === 1000);
console.log('[Terminal] Connection attempt:', this.reconnectAttempts + 1);
logTerminal('warn', `[Terminal] WebSocket connection closed. Code: ${event.code}, Reason: ${event.reason || 'No reason provided'}`);
logTerminal('log', '[Terminal] Was clean close:', event.code === 1000);
logTerminal('log', '[Terminal] Connection attempt:', this.reconnectAttempts + 1);
this.connectionState = 'disconnected';
this.clearAllTimers();
@@ -297,7 +317,7 @@ export function initializeTerminalComponent() {
},
handleConnectionError(reason) {
console.error(`[Terminal] Connection error: ${reason} (attempt ${this.reconnectAttempts + 1})`);
logTerminal('error', `[Terminal] Connection error: ${reason} (attempt ${this.reconnectAttempts + 1})`);
this.connectionState = 'disconnected';
// Only dispatch error to UI after a few failed attempts to avoid immediate error on page load
@@ -310,7 +330,7 @@ export function initializeTerminalComponent() {
scheduleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('[Terminal] Max reconnection attempts reached');
logTerminal('error', '[Terminal] Max reconnection attempts reached');
this.message = '(connection failed - max retries exceeded)';
return;
}
@@ -323,7 +343,7 @@ export function initializeTerminalComponent() {
this.maxReconnectDelay
);
console.warn(`[Terminal] Scheduling reconnect attempt ${this.reconnectAttempts + 1} in ${delay}ms`);
logTerminal('warn', `[Terminal] Scheduling reconnect attempt ${this.reconnectAttempts + 1} in ${delay}ms`);
this.reconnectInterval = setTimeout(() => {
this.reconnectAttempts++;
@@ -335,17 +355,21 @@ export function initializeTerminalComponent() {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(message));
} else {
console.warn('[Terminal] WebSocket not ready, message not sent:', message);
logTerminal('warn', '[Terminal] WebSocket not ready, message not sent:', message);
}
},
sendCommandWhenReady(message) {
if (this.isWebSocketReady()) {
this.sendMessage(message);
} else {
this.pendingCommand = message;
}
},
handleSocketMessage(event) {
logTerminal('log', '[Terminal] Received WebSocket message:', event.data);
// Handle pong responses
if (event.data === 'pong') {
this.heartbeatMissed = 0;
@@ -354,6 +378,10 @@ export function initializeTerminalComponent() {
return;
}
if (!this.term?._initialized && event.data !== 'pty-ready') {
logTerminal('warn', '[Terminal] Received message before PTY initialization:', event.data);
}
if (event.data === 'pty-ready') {
if (!this.term._initialized) {
this.term.open(document.getElementById('terminal'));
@@ -398,17 +426,24 @@ export function initializeTerminalComponent() {
// Notify parent component that terminal disconnected
this.$wire.dispatch('terminalDisconnected');
} else if (
typeof event.data === 'string' &&
(event.data.startsWith('Unauthorized:') || event.data.startsWith('Invalid SSH command:'))
) {
logTerminal('error', '[Terminal] Backend rejected terminal startup:', event.data);
this.$wire.dispatch('error', event.data);
this.terminalActive = false;
} else {
try {
this.pendingWrites++;
this.term.write(event.data, (err) => {
if (err) {
console.error('[Terminal] Write error:', err);
logTerminal('error', '[Terminal] Write error:', err);
}
this.flowControlCallback();
});
} catch (error) {
console.error('[Terminal] Write operation failed:', error);
logTerminal('error', '[Terminal] Write operation failed:', error);
this.pendingWrites = Math.max(0, this.pendingWrites - 1);
}
}
@@ -483,10 +518,10 @@ export function initializeTerminalComponent() {
clearTimeout(this.pingTimeoutId);
this.pingTimeoutId = null;
}
console.log('[Terminal] Tab hidden, pausing heartbeat monitoring');
logTerminal('log', '[Terminal] Tab hidden, pausing heartbeat monitoring');
} else if (wasVisible === false) {
// Tab is now visible again
console.log('[Terminal] Tab visible, resuming connection management');
logTerminal('log', '[Terminal] Tab visible, resuming connection management');
if (this.wasConnectedBeforeHidden && this.socket && this.socket.readyState === WebSocket.OPEN) {
// Send immediate ping to verify connection is still alive
@@ -508,10 +543,10 @@ export function initializeTerminalComponent() {
this.pingTimeoutId = setTimeout(() => {
this.heartbeatMissed++;
console.warn(`[Terminal] Ping timeout - missed ${this.heartbeatMissed}/${this.maxHeartbeatMisses}`);
logTerminal('warn', `[Terminal] Ping timeout - missed ${this.heartbeatMissed}/${this.maxHeartbeatMisses}`);
if (this.heartbeatMissed >= this.maxHeartbeatMisses) {
console.error('[Terminal] Too many missed heartbeats, closing connection');
logTerminal('error', '[Terminal] Too many missed heartbeats, closing connection');
this.socket.close(1001, 'Heartbeat timeout');
}
}, this.pingTimeout);
@@ -553,7 +588,7 @@ export function initializeTerminalComponent() {
// Check if dimensions are valid
if (height <= 0 || width <= 0) {
console.warn('[Terminal] Invalid wrapper dimensions, retrying...', { height, width });
logTerminal('warn', '[Terminal] Invalid wrapper dimensions, retrying...', { height, width });
setTimeout(() => this.resizeTerminal(), 100);
return;
}
@@ -562,7 +597,7 @@ export function initializeTerminalComponent() {
if (!charSize.height || !charSize.width) {
// Fallback values if char size not available yet
console.warn('[Terminal] Character size not available, retrying...');
logTerminal('warn', '[Terminal] Character size not available, retrying...');
setTimeout(() => this.resizeTerminal(), 100);
return;
}
@@ -583,10 +618,10 @@ export function initializeTerminalComponent() {
});
}
} else {
console.warn('[Terminal] Invalid calculated dimensions:', { rows, cols, height, width, charSize });
logTerminal('warn', '[Terminal] Invalid calculated dimensions:', { rows, cols, height, width, charSize });
}
} catch (error) {
console.error('[Terminal] Resize error:', error);
logTerminal('error', '[Terminal] Resize error:', error);
}
},
@@ -21,7 +21,8 @@
<div>No containers are running or terminal access is disabled on this server.</div>
@else
<form class="w-96 min-w-fit flex gap-2 items-end" wire:submit="$dispatchSelf('connectToContainer')"
x-data="{ autoConnected: false }" x-init="if ({{ count($containers) }} === 1 && !autoConnected) {
x-data="{ autoConnected: false }"
x-on:terminal-websocket-ready.window="if ({{ count($containers) }} === 1 && !autoConnected) {
autoConnected = true;
$nextTick(() => $wire.dispatchSelf('connectToContainer'));
}">