feat: Implement network status banner

This commit is contained in:
gpt-engineer-app[bot]
2025-11-05 14:12:23 +00:00
parent 783284a47a
commit e773ca58d1
4 changed files with 156 additions and 0 deletions

View File

@@ -18,6 +18,7 @@ import { EntityErrorBoundary } from "@/components/error/EntityErrorBoundary";
import { breadcrumb } from "@/lib/errorBreadcrumbs";
import { handleError } from "@/lib/errorHandler";
import { RetryStatusIndicator } from "@/components/ui/retry-status-indicator";
import { NetworkStatusBanner } from "@/components/ui/network-status-banner";
// Core routes (eager-loaded for best UX)
import Index from "./pages/Index";
@@ -134,6 +135,7 @@ function AppContent(): React.JSX.Element {
<TooltipProvider>
<NavigationTracker />
<LocationAutoDetectProvider />
<NetworkStatusBanner />
<RetryStatusIndicator />
<Toaster />
<Sonner />

View File

@@ -0,0 +1,83 @@
import { useEffect, useState } from 'react';
import { XCircle, Loader2 } from 'lucide-react';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { useCircuitBreakerStatus } from '@/hooks/useCircuitBreakerStatus';
import { CircuitState } from '@/lib/circuitBreaker';
import { cn } from '@/lib/utils';
export function NetworkStatusBanner() {
const { state, failureCount, isOpen, isHalfOpen } = useCircuitBreakerStatus();
const [countdown, setCountdown] = useState(60);
// Countdown for next retry attempt (when OPEN)
useEffect(() => {
if (!isOpen) return;
setCountdown(60); // Reset timeout from circuit breaker config
const interval = setInterval(() => {
setCountdown(prev => Math.max(0, prev - 1));
}, 1000);
return () => clearInterval(interval);
}, [isOpen]);
// Don't show if circuit is closed
if (state === CircuitState.CLOSED) return null;
// OPEN state - critical error
if (isOpen) {
return (
<Alert
role="alert"
aria-live="assertive"
aria-atomic="true"
variant="destructive"
className={cn(
"rounded-none border-x-0 border-t-0",
"animate-in slide-in-from-top-4 duration-300",
"bg-destructive/90 backdrop-blur-sm"
)}
>
<XCircle className="h-5 w-5 animate-pulse" />
<AlertTitle className="font-semibold text-lg">
Database Unavailable
</AlertTitle>
<AlertDescription className="text-sm mt-1">
Our systems detected a service outage. <strong>Data and authentication are temporarily unavailable.</strong>
{' '}Retrying automatically... (next attempt in {countdown}s)
{failureCount > 0 && (
<span className="block mt-1 text-xs opacity-80">
{failureCount} failed connection attempts detected
</span>
)}
</AlertDescription>
</Alert>
);
}
// HALF_OPEN state - testing recovery
if (isHalfOpen) {
return (
<Alert
role="alert"
aria-live="polite"
aria-atomic="true"
className={cn(
"rounded-none border-x-0 border-t-0",
"animate-in slide-in-from-top-4 duration-300",
"bg-amber-500/20 dark:bg-amber-500/30 border-amber-500"
)}
>
<Loader2 className="h-5 w-5 animate-spin text-amber-600 dark:text-amber-400" />
<AlertTitle className="font-semibold text-amber-900 dark:text-amber-100">
Connection Unstable
</AlertTitle>
<AlertDescription className="text-sm text-amber-800 dark:text-amber-200 mt-1">
Testing database connection... Some features may be slow or temporarily unavailable.
</AlertDescription>
</Alert>
);
}
return null;
}

View File

@@ -0,0 +1,54 @@
import { useState, useEffect } from 'react';
import { supabaseCircuitBreaker, CircuitState } from '@/lib/circuitBreaker';
import { logger } from '@/lib/logger';
export function useCircuitBreakerStatus() {
const [state, setState] = useState<CircuitState>(CircuitState.CLOSED);
const [failureCount, setFailureCount] = useState(0);
const [lastStateChange, setLastStateChange] = useState<Date>(new Date());
useEffect(() => {
// Check immediately on mount
const checkState = () => {
const currentState = supabaseCircuitBreaker.getState();
const currentFailures = supabaseCircuitBreaker.getFailureCount();
setState(prevState => {
if (prevState !== currentState) {
setLastStateChange(new Date());
// Log state changes for monitoring
logger.info('Circuit breaker state changed', {
from: prevState,
to: currentState,
failureCount: currentFailures
});
// Emit custom event for other components
window.dispatchEvent(new CustomEvent('circuit-breaker-state-change', {
detail: { state: currentState, failureCount: currentFailures }
}));
}
return currentState;
});
setFailureCount(currentFailures);
};
checkState();
// Poll every 5 seconds
const interval = setInterval(checkState, 5000);
return () => clearInterval(interval);
}, []);
return {
state,
failureCount,
lastStateChange,
isOpen: state === CircuitState.OPEN,
isHalfOpen: state === CircuitState.HALF_OPEN,
isClosed: state === CircuitState.CLOSED
};
}

View File

@@ -98,6 +98,13 @@ export class CircuitBreaker {
logger.info('Circuit breaker CLOSED - service recovered', {
successCount: this.successCount
});
// Emit event for UI components
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('circuit-breaker-closed', {
detail: { successCount: this.successCount }
}));
}
}
}
}
@@ -122,6 +129,16 @@ export class CircuitBreaker {
monitoringWindow: this.config.monitoringWindow,
resetTimeout: this.config.resetTimeout
});
// Emit event for UI components
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('circuit-breaker-opened', {
detail: {
failures: this.failures.length,
threshold: this.config.failureThreshold
}
}));
}
}
}