Fix api routes and missing translations and improve reconnection for terminals
This commit is contained in:
@@ -306,9 +306,6 @@ export function Server({
|
||||
value={typeof metrics?.cpu?.percent === 'number' ? metrics!.cpu!.percent! : 0}
|
||||
className="h-2"
|
||||
/>
|
||||
{typeof metrics?.cpu?.percent === 'number' && metrics.cpu.percent > 80 && (
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-500">
|
||||
@@ -347,9 +344,6 @@ export function Server({
|
||||
value={typeof metrics?.memory?.percent === 'number' ? metrics!.memory!.percent! : 0}
|
||||
className="h-2"
|
||||
/>
|
||||
{typeof metrics?.memory?.percent === 'number' && metrics.memory.percent > 85 && (
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-500">
|
||||
@@ -390,9 +384,6 @@ export function Server({
|
||||
value={typeof metrics?.disk?.percent === 'number' ? metrics!.disk!.percent! : 0}
|
||||
className="h-2"
|
||||
/>
|
||||
{typeof metrics?.disk?.percent === 'number' && metrics.disk.percent > 90 && (
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-500">
|
||||
|
||||
@@ -29,6 +29,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
const pingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [connectionError, setConnectionError] = useState<string | null>(null);
|
||||
const isVisibleRef = useRef<boolean>(false);
|
||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
@@ -36,7 +37,8 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
const maxReconnectAttempts = 3;
|
||||
const isUnmountingRef = useRef(false);
|
||||
const shouldNotReconnectRef = useRef(false);
|
||||
|
||||
const isReconnectingRef = useRef(false);
|
||||
const connectionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
||||
const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
||||
@@ -76,6 +78,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
disconnect: () => {
|
||||
isUnmountingRef.current = true;
|
||||
shouldNotReconnectRef.current = true;
|
||||
isReconnectingRef.current = false;
|
||||
if (pingIntervalRef.current) {
|
||||
clearInterval(pingIntervalRef.current);
|
||||
pingIntervalRef.current = null;
|
||||
@@ -84,8 +87,13 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
clearTimeout(reconnectTimeoutRef.current);
|
||||
reconnectTimeoutRef.current = null;
|
||||
}
|
||||
if (connectionTimeoutRef.current) {
|
||||
clearTimeout(connectionTimeoutRef.current);
|
||||
connectionTimeoutRef.current = null;
|
||||
}
|
||||
webSocketRef.current?.close();
|
||||
setIsConnected(false);
|
||||
setIsConnecting(false); // Clear connecting state
|
||||
},
|
||||
fit: () => {
|
||||
fitAddonRef.current?.fit();
|
||||
@@ -135,31 +143,54 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
}
|
||||
|
||||
function attemptReconnection() {
|
||||
// Don't attempt reconnection if component is unmounting or if we shouldn't reconnect
|
||||
if (isUnmountingRef.current || shouldNotReconnectRef.current) {
|
||||
// Don't attempt reconnection if component is unmounting, shouldn't reconnect, or already reconnecting
|
||||
if (isUnmountingRef.current || shouldNotReconnectRef.current || isReconnectingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we've already reached max attempts
|
||||
if (reconnectAttempts.current >= maxReconnectAttempts) {
|
||||
toast.error(t('terminal.maxReconnectAttemptsReached'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Set reconnecting flag to prevent multiple simultaneous attempts
|
||||
isReconnectingRef.current = true;
|
||||
|
||||
// Clear terminal immediately to prevent showing last line
|
||||
if (terminal) {
|
||||
terminal.clear();
|
||||
}
|
||||
|
||||
// Increment attempt counter
|
||||
reconnectAttempts.current++;
|
||||
|
||||
// Show toast with current attempt number
|
||||
toast.info(t('terminal.reconnecting', { attempt: reconnectAttempts.current, max: maxReconnectAttempts }));
|
||||
|
||||
reconnectTimeoutRef.current = setTimeout(() => {
|
||||
// Check again if component is still mounted and should reconnect
|
||||
if (isUnmountingRef.current || shouldNotReconnectRef.current) {
|
||||
isReconnectingRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we haven't exceeded max attempts during the timeout
|
||||
if (reconnectAttempts.current > maxReconnectAttempts) {
|
||||
isReconnectingRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (terminal && hostConfig) {
|
||||
// Clear terminal before reconnecting
|
||||
// Ensure terminal is clear before reconnecting
|
||||
terminal.clear();
|
||||
const cols = terminal.cols;
|
||||
const rows = terminal.rows;
|
||||
connectToHost(cols, rows);
|
||||
}
|
||||
|
||||
// Reset reconnecting flag after attempting connection
|
||||
isReconnectingRef.current = false;
|
||||
}, 2000 * reconnectAttempts.current); // Exponential backoff
|
||||
}
|
||||
|
||||
@@ -187,6 +218,8 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
wasDisconnectedBySSH.current = false;
|
||||
setConnectionError(null);
|
||||
shouldNotReconnectRef.current = false; // Reset reconnection flag
|
||||
isReconnectingRef.current = false; // Reset reconnecting flag
|
||||
setIsConnecting(true); // Set connecting state
|
||||
|
||||
setupWebSocketListeners(ws, cols, rows);
|
||||
}
|
||||
@@ -195,12 +228,27 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
|
||||
function setupWebSocketListeners(ws: WebSocket, cols: number, rows: number) {
|
||||
ws.addEventListener('open', () => {
|
||||
setIsConnected(true);
|
||||
// Show reconnected toast if this was a reconnection attempt
|
||||
if (reconnectAttempts.current > 0) {
|
||||
toast.success(t('terminal.reconnected'));
|
||||
}
|
||||
reconnectAttempts.current = 0; // Reset on successful connection
|
||||
// Don't set isConnected to true here - wait for actual SSH connection
|
||||
// Don't show reconnected toast here - wait for actual connection confirmation
|
||||
|
||||
// Set a timeout for SSH connection establishment
|
||||
connectionTimeoutRef.current = setTimeout(() => {
|
||||
if (!isConnected) {
|
||||
// SSH connection didn't establish within timeout
|
||||
// Clear terminal immediately when connection times out
|
||||
if (terminal) {
|
||||
terminal.clear();
|
||||
}
|
||||
toast.error(t('terminal.connectionTimeout'));
|
||||
if (webSocketRef.current) {
|
||||
webSocketRef.current.close();
|
||||
}
|
||||
// Attempt reconnection if this was a reconnection attempt
|
||||
if (reconnectAttempts.current > 0) {
|
||||
attemptReconnection();
|
||||
}
|
||||
}
|
||||
}, 10000); // 10 second timeout for SSH connection
|
||||
|
||||
ws.send(JSON.stringify({type: 'connectToHost', data: {cols, rows, hostConfig}}));
|
||||
terminal.onData((data) => {
|
||||
@@ -250,6 +298,12 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
errorMessage.toLowerCase().includes('network')) {
|
||||
toast.error(t('terminal.connectionError', { message: errorMessage }));
|
||||
setIsConnected(false);
|
||||
// Clear terminal immediately when connection error occurs
|
||||
if (terminal) {
|
||||
terminal.clear();
|
||||
}
|
||||
// Set connecting state immediately for reconnection
|
||||
setIsConnecting(true);
|
||||
attemptReconnection();
|
||||
return;
|
||||
}
|
||||
@@ -258,9 +312,28 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
toast.error(t('terminal.error', { message: errorMessage }));
|
||||
} else if (msg.type === 'connected') {
|
||||
setIsConnected(true);
|
||||
setIsConnecting(false); // Clear connecting state
|
||||
// Clear connection timeout since SSH connection is established
|
||||
if (connectionTimeoutRef.current) {
|
||||
clearTimeout(connectionTimeoutRef.current);
|
||||
connectionTimeoutRef.current = null;
|
||||
}
|
||||
// Show reconnected toast if this was a reconnection attempt
|
||||
if (reconnectAttempts.current > 0) {
|
||||
toast.success(t('terminal.reconnected'));
|
||||
}
|
||||
// Reset reconnection counter and flags on successful connection
|
||||
reconnectAttempts.current = 0;
|
||||
isReconnectingRef.current = false;
|
||||
} else if (msg.type === 'disconnected') {
|
||||
wasDisconnectedBySSH.current = true;
|
||||
setIsConnected(false);
|
||||
// Clear terminal immediately when disconnected
|
||||
if (terminal) {
|
||||
terminal.clear();
|
||||
}
|
||||
// Set connecting state immediately for reconnection
|
||||
setIsConnecting(true);
|
||||
// Attempt reconnection for disconnections
|
||||
if (!isUnmountingRef.current && !shouldNotReconnectRef.current) {
|
||||
attemptReconnection();
|
||||
@@ -273,6 +346,12 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
|
||||
ws.addEventListener('close', (event) => {
|
||||
setIsConnected(false);
|
||||
// Clear terminal immediately when connection closes
|
||||
if (terminal) {
|
||||
terminal.clear();
|
||||
}
|
||||
// Set connecting state immediately for reconnection
|
||||
setIsConnecting(true);
|
||||
if (!wasDisconnectedBySSH.current && !isUnmountingRef.current && !shouldNotReconnectRef.current) {
|
||||
// Attempt reconnection for unexpected disconnections
|
||||
attemptReconnection();
|
||||
@@ -282,6 +361,12 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
ws.addEventListener('error', (event) => {
|
||||
setIsConnected(false);
|
||||
setConnectionError(t('terminal.websocketError'));
|
||||
// Clear terminal immediately when WebSocket error occurs
|
||||
if (terminal) {
|
||||
terminal.clear();
|
||||
}
|
||||
// Set connecting state immediately for reconnection
|
||||
setIsConnecting(true);
|
||||
// Attempt reconnection for WebSocket errors
|
||||
if (!isUnmountingRef.current && !shouldNotReconnectRef.current) {
|
||||
attemptReconnection();
|
||||
@@ -429,11 +514,14 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
return () => {
|
||||
isUnmountingRef.current = true;
|
||||
shouldNotReconnectRef.current = true;
|
||||
isReconnectingRef.current = false;
|
||||
setIsConnecting(false); // Clear connecting state
|
||||
resizeObserver.disconnect();
|
||||
element?.removeEventListener('contextmenu', handleContextMenu);
|
||||
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
|
||||
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
|
||||
if (reconnectTimeoutRef.current) clearTimeout(reconnectTimeoutRef.current);
|
||||
if (connectionTimeoutRef.current) clearTimeout(connectionTimeoutRef.current);
|
||||
if (pingIntervalRef.current) {
|
||||
clearInterval(pingIntervalRef.current);
|
||||
pingIntervalRef.current = null;
|
||||
@@ -474,15 +562,28 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
}, [splitScreen, isVisible, terminal]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={xtermRef}
|
||||
className={`h-full w-full m-1 transition-opacity duration-200 ${visible && isVisible ? 'opacity-100' : 'opacity-0'} overflow-hidden`}
|
||||
onClick={() => {
|
||||
if (terminal && !splitScreen) {
|
||||
terminal.focus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="h-full w-full m-1 relative">
|
||||
{/* Terminal */}
|
||||
<div
|
||||
ref={xtermRef}
|
||||
className={`h-full w-full transition-opacity duration-200 ${visible && isVisible && !isConnecting ? 'opacity-100' : 'opacity-0'} overflow-hidden`}
|
||||
onClick={() => {
|
||||
if (terminal && !splitScreen) {
|
||||
terminal.focus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Connecting State */}
|
||||
{isConnecting && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-dark-bg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-6 h-6 border-2 border-blue-400 border-t-transparent rounded-full animate-spin"></div>
|
||||
<span className="text-gray-300">{t('terminal.connecting')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -115,7 +115,6 @@ function AppContent() {
|
||||
isAuthenticated={isAuthenticated}
|
||||
authLoading={authLoading}
|
||||
onAuthSuccess={handleAuthSuccess}
|
||||
isTopbarOpen={isTopbarOpen}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -11,7 +11,6 @@ interface HomepageProps {
|
||||
isAuthenticated: boolean;
|
||||
authLoading: boolean;
|
||||
onAuthSuccess: (authData: { isAdmin: boolean; username: string | null; userId: string | null }) => void;
|
||||
isTopbarOpen?: boolean;
|
||||
}
|
||||
|
||||
function getCookie(name: string) {
|
||||
@@ -30,8 +29,7 @@ export function Homepage({
|
||||
onSelectView,
|
||||
isAuthenticated,
|
||||
authLoading,
|
||||
onAuthSuccess,
|
||||
isTopbarOpen = true
|
||||
onAuthSuccess
|
||||
}: HomepageProps): React.ReactElement {
|
||||
const {t} = useTranslation();
|
||||
const [loggedIn, setLoggedIn] = useState(isAuthenticated);
|
||||
@@ -72,82 +70,64 @@ export function Homepage({
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
const topOffset = isTopbarOpen ? 66 : 0;
|
||||
const topPadding = isTopbarOpen ? 66 : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full min-h-svh relative transition-[padding-top] duration-300 ease-in-out"
|
||||
style={{ paddingTop: `${topPadding}px` }}>
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
{!loggedIn ? (
|
||||
<div
|
||||
className="absolute left-0 w-full flex items-center justify-center"
|
||||
style={{
|
||||
top: `${topOffset}px`,
|
||||
height: `calc(100% - ${topOffset}px)`
|
||||
}}>
|
||||
<HomepageAuth
|
||||
setLoggedIn={setLoggedIn}
|
||||
setIsAdmin={setIsAdmin}
|
||||
setUsername={setUsername}
|
||||
setUserId={setUserId}
|
||||
loggedIn={loggedIn}
|
||||
authLoading={authLoading}
|
||||
dbError={dbError}
|
||||
setDbError={setDbError}
|
||||
onAuthSuccess={onAuthSuccess}
|
||||
/>
|
||||
</div>
|
||||
<HomepageAuth
|
||||
setLoggedIn={setLoggedIn}
|
||||
setIsAdmin={setIsAdmin}
|
||||
setUsername={setUsername}
|
||||
setUserId={setUserId}
|
||||
loggedIn={loggedIn}
|
||||
authLoading={authLoading}
|
||||
dbError={dbError}
|
||||
setDbError={setDbError}
|
||||
onAuthSuccess={onAuthSuccess}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="absolute left-0 w-full flex items-center justify-center"
|
||||
style={{
|
||||
top: `${topOffset}px`,
|
||||
height: `calc(100% - ${topOffset}px)`
|
||||
}}>
|
||||
<div className="flex flex-row items-center justify-center gap-8 relative z-10">
|
||||
<div className="flex flex-col items-center gap-6 w-[400px]">
|
||||
<HomepageUpdateLog
|
||||
loggedIn={loggedIn}
|
||||
/>
|
||||
<div className="flex flex-row items-center justify-center gap-8 relative z-10">
|
||||
<div className="flex flex-col items-center gap-6 w-[400px]">
|
||||
<HomepageUpdateLog
|
||||
loggedIn={loggedIn}
|
||||
/>
|
||||
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||
onClick={() => window.open('https://github.com/LukeGus/Termix', '_blank')}
|
||||
>
|
||||
GitHub
|
||||
</Button>
|
||||
<div className="w-px h-4 bg-dark-border"></div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||
onClick={() => window.open('https://github.com/LukeGus/Termix/issues/new', '_blank')}
|
||||
>
|
||||
Feedback
|
||||
</Button>
|
||||
<div className="w-px h-4 bg-dark-border"></div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||
onClick={() => window.open('https://discord.com/invite/jVQGdvHDrf', '_blank')}
|
||||
>
|
||||
Discord
|
||||
</Button>
|
||||
<div className="w-px h-4 bg-dark-border"></div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||
onClick={() => window.open('https://github.com/sponsors/LukeGus', '_blank')}
|
||||
>
|
||||
Donate
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||
onClick={() => window.open('https://github.com/LukeGus/Termix', '_blank')}
|
||||
>
|
||||
GitHub
|
||||
</Button>
|
||||
<div className="w-px h-4 bg-dark-border"></div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||
onClick={() => window.open('https://github.com/LukeGus/Termix/issues/new', '_blank')}
|
||||
>
|
||||
Feedback
|
||||
</Button>
|
||||
<div className="w-px h-4 bg-dark-border"></div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||
onClick={() => window.open('https://discord.com/invite/jVQGdvHDrf', '_blank')}
|
||||
>
|
||||
Discord
|
||||
</Button>
|
||||
<div className="w-px h-4 bg-dark-border"></div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||
onClick={() => window.open('https://github.com/sponsors/LukeGus', '_blank')}
|
||||
>
|
||||
Donate
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user