ALwrity Backend and Frontend - Stability and Error Handling Improvements
This commit is contained in:
498
frontend/src/components/shared/BackendConnectionError.tsx
Normal file
498
frontend/src/components/shared/BackendConnectionError.tsx
Normal file
@@ -0,0 +1,498 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Paper,
|
||||
Typography,
|
||||
Button,
|
||||
Stack,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
LinearProgress,
|
||||
Fade,
|
||||
Chip,
|
||||
Card,
|
||||
CardContent,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText
|
||||
} from '@mui/material';
|
||||
import {
|
||||
WifiOff as ConnectionIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Home as HomeIcon,
|
||||
Settings as SettingsIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Error as ErrorIcon,
|
||||
Schedule as ScheduleIcon,
|
||||
CloudQueue as CloudIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface BackendConnectionErrorProps {
|
||||
error?: string;
|
||||
onRetry?: () => void;
|
||||
onGoHome?: () => void;
|
||||
}
|
||||
|
||||
interface ConnectionAttempt {
|
||||
timestamp: number;
|
||||
success: boolean;
|
||||
responseTime?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced Backend Connection Error Component
|
||||
*
|
||||
* Shows a loading state for 2 minutes while attempting to reconnect,
|
||||
* then gracefully shows error if backend remains unresponsive.
|
||||
*/
|
||||
const BackendConnectionError: React.FC<BackendConnectionErrorProps> = ({
|
||||
error = 'Backend service is not available. Please check if the server is running.',
|
||||
onRetry,
|
||||
onGoHome
|
||||
}) => {
|
||||
const [isRetrying, setIsRetrying] = useState(true);
|
||||
const [timeElapsed, setTimeElapsed] = useState(0);
|
||||
const [connectionAttempts, setConnectionAttempts] = useState<ConnectionAttempt[]>([]);
|
||||
const [showError, setShowError] = useState(false);
|
||||
|
||||
const MAX_RETRY_TIME = 120; // 2 minutes in seconds
|
||||
const RETRY_INTERVAL = 5000; // 5 seconds
|
||||
|
||||
// Timer for elapsed time
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setTimeElapsed(prev => {
|
||||
const newTime = prev + 1;
|
||||
if (newTime >= MAX_RETRY_TIME) {
|
||||
setIsRetrying(false);
|
||||
setShowError(true);
|
||||
return MAX_RETRY_TIME;
|
||||
}
|
||||
return newTime;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
// Retry attempts
|
||||
useEffect(() => {
|
||||
if (!isRetrying) return;
|
||||
|
||||
const retryTimer = setInterval(async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Try to connect to a simple health check endpoint with short timeout
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 2000); // 2 second timeout
|
||||
|
||||
const response = await fetch('/health', {
|
||||
method: 'GET',
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
setConnectionAttempts(prev => [...prev, {
|
||||
timestamp: Date.now(),
|
||||
success: response.ok,
|
||||
responseTime
|
||||
}]);
|
||||
|
||||
if (response.ok) {
|
||||
// Backend is back online, trigger retry callback
|
||||
setIsRetrying(false);
|
||||
setShowError(false);
|
||||
if (onRetry) {
|
||||
onRetry();
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (error: any) {
|
||||
setConnectionAttempts(prev => [...prev, {
|
||||
timestamp: Date.now(),
|
||||
success: false,
|
||||
responseTime: Date.now() - startTime
|
||||
}]);
|
||||
}
|
||||
}, RETRY_INTERVAL);
|
||||
|
||||
return () => clearInterval(retryTimer);
|
||||
}, [isRetrying, onRetry]);
|
||||
|
||||
const progress = (timeElapsed / MAX_RETRY_TIME) * 100;
|
||||
const minutes = Math.floor(timeElapsed / 60);
|
||||
const seconds = timeElapsed % 60;
|
||||
const timeString = `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
|
||||
const handleManualRetry = () => {
|
||||
setIsRetrying(true);
|
||||
setShowError(false);
|
||||
setTimeElapsed(0);
|
||||
setConnectionAttempts([]);
|
||||
};
|
||||
|
||||
const handleGoHome = () => {
|
||||
if (onGoHome) {
|
||||
onGoHome();
|
||||
} else {
|
||||
window.location.href = '/';
|
||||
}
|
||||
};
|
||||
|
||||
if (showError) {
|
||||
// Show final error state after 2 minutes
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
p: { xs: 2, md: 4 },
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="md">
|
||||
<Paper
|
||||
elevation={24}
|
||||
sx={{
|
||||
p: { xs: 3, md: 5 },
|
||||
borderRadius: 4,
|
||||
background: 'rgba(255, 255, 255, 0.98)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.3)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4} alignItems="center">
|
||||
{/* Error Icon */}
|
||||
<Box
|
||||
sx={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(45deg, #f44336 30%, #e91e63 90%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 8px 32px rgba(244, 67, 54, 0.3)',
|
||||
}}
|
||||
>
|
||||
<ConnectionIcon sx={{ fontSize: 48, color: 'white' }} />
|
||||
</Box>
|
||||
|
||||
{/* Error Title */}
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
color: '#1a1a1a',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Connection Error
|
||||
</Typography>
|
||||
|
||||
{/* Error Message */}
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="text.secondary"
|
||||
sx={{ textAlign: 'center', maxWidth: 600 }}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
|
||||
{/* Troubleshooting Tips */}
|
||||
<Card sx={{ width: '100%', maxWidth: 600 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<SettingsIcon />
|
||||
Troubleshooting Steps
|
||||
</Typography>
|
||||
<List dense>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Check if the backend server is running" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Verify the backend is accessible on the correct port" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Check firewall and network settings" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Review backend server logs for errors" />
|
||||
</ListItem>
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
spacing={2}
|
||||
sx={{ width: '100%', maxWidth: 400 }}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={handleManualRetry}
|
||||
sx={{
|
||||
flex: 1,
|
||||
py: 1.5,
|
||||
background: 'linear-gradient(45deg, #667eea 30%, #764ba2 90%)',
|
||||
'&:hover': {
|
||||
background: 'linear-gradient(45deg, #5568d3 30%, #6a3f8f 90%)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
startIcon={<HomeIcon />}
|
||||
onClick={handleGoHome}
|
||||
sx={{
|
||||
flex: 1,
|
||||
py: 1.5,
|
||||
borderColor: '#667eea',
|
||||
color: '#667eea',
|
||||
'&:hover': {
|
||||
borderColor: '#5568d3',
|
||||
background: 'rgba(102, 126, 234, 0.05)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Go Home
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
{/* Connection Attempts Summary */}
|
||||
{connectionAttempts.length > 0 && (
|
||||
<Box sx={{ width: '100%', maxWidth: 400 }}>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
Connection attempts: {connectionAttempts.length}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Last attempt: {connectionAttempts[connectionAttempts.length - 1]?.success ? 'Successful' : 'Failed'}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Show loading state for first 2 minutes
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
p: { xs: 2, md: 4 },
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="md">
|
||||
<Paper
|
||||
elevation={24}
|
||||
sx={{
|
||||
p: { xs: 3, md: 5 },
|
||||
borderRadius: 4,
|
||||
background: 'rgba(255, 255, 255, 0.98)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.3)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4} alignItems="center">
|
||||
{/* Loading Animation */}
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<CircularProgress
|
||||
size={80}
|
||||
thickness={4}
|
||||
sx={{
|
||||
color: '#667eea',
|
||||
animation: 'pulse 2s ease-in-out infinite',
|
||||
'@keyframes pulse': {
|
||||
'0%': { opacity: 1 },
|
||||
'50%': { opacity: 0.5 },
|
||||
'100%': { opacity: 1 },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<CloudIcon sx={{ fontSize: 32, color: '#667eea' }} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Loading Title */}
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
color: '#1a1a1a',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Connecting to Backend
|
||||
</Typography>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<Box sx={{ width: '100%', maxWidth: 400 }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
sx={{
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: 'rgba(102, 126, 234, 0.2)',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
backgroundColor: '#667eea',
|
||||
borderRadius: 4,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Time and Progress Info */}
|
||||
<Stack spacing={1} alignItems="center">
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Attempting to reconnect... {timeString}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={`${Math.round(progress)}% complete`}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{/* Motivational Messages */}
|
||||
<Fade in timeout={1000}>
|
||||
<Alert
|
||||
severity="info"
|
||||
icon={<ScheduleIcon />}
|
||||
sx={{
|
||||
maxWidth: 500,
|
||||
textAlign: 'center',
|
||||
'& .MuiAlert-message': {
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2">
|
||||
{timeElapsed < 60
|
||||
? "We're working to restore your connection..."
|
||||
: "Still trying to connect. This may take a moment..."
|
||||
}
|
||||
</Typography>
|
||||
</Alert>
|
||||
</Fade>
|
||||
|
||||
{/* Connection Attempts */}
|
||||
{connectionAttempts.length > 0 && (
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
Connection attempts: {connectionAttempts.length}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{connectionAttempts.filter(attempt => attempt.success).length} successful,{' '}
|
||||
{connectionAttempts.filter(attempt => !attempt.success).length} failed
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
spacing={2}
|
||||
sx={{ width: '100%', maxWidth: 400 }}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={handleManualRetry}
|
||||
sx={{
|
||||
flex: 1,
|
||||
py: 1.5,
|
||||
borderColor: '#667eea',
|
||||
color: '#667eea',
|
||||
'&:hover': {
|
||||
borderColor: '#5568d3',
|
||||
background: 'rgba(102, 126, 234, 0.05)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Retry Now
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="text"
|
||||
size="large"
|
||||
startIcon={<HomeIcon />}
|
||||
onClick={handleGoHome}
|
||||
sx={{
|
||||
flex: 1,
|
||||
py: 1.5,
|
||||
color: '#666',
|
||||
'&:hover': {
|
||||
background: 'rgba(0, 0, 0, 0.05)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Go Home
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
{/* Help Text */}
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ textAlign: 'center', maxWidth: 400 }}
|
||||
>
|
||||
If this issue persists, please check your internet connection and ensure the backend server is running.
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackendConnectionError;
|
||||
Reference in New Issue
Block a user