mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 11:51:14 -05:00
feat: Implement network status banner
This commit is contained in:
@@ -18,6 +18,7 @@ import { EntityErrorBoundary } from "@/components/error/EntityErrorBoundary";
|
|||||||
import { breadcrumb } from "@/lib/errorBreadcrumbs";
|
import { breadcrumb } from "@/lib/errorBreadcrumbs";
|
||||||
import { handleError } from "@/lib/errorHandler";
|
import { handleError } from "@/lib/errorHandler";
|
||||||
import { RetryStatusIndicator } from "@/components/ui/retry-status-indicator";
|
import { RetryStatusIndicator } from "@/components/ui/retry-status-indicator";
|
||||||
|
import { NetworkStatusBanner } from "@/components/ui/network-status-banner";
|
||||||
|
|
||||||
// Core routes (eager-loaded for best UX)
|
// Core routes (eager-loaded for best UX)
|
||||||
import Index from "./pages/Index";
|
import Index from "./pages/Index";
|
||||||
@@ -134,6 +135,7 @@ function AppContent(): React.JSX.Element {
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<NavigationTracker />
|
<NavigationTracker />
|
||||||
<LocationAutoDetectProvider />
|
<LocationAutoDetectProvider />
|
||||||
|
<NetworkStatusBanner />
|
||||||
<RetryStatusIndicator />
|
<RetryStatusIndicator />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<Sonner />
|
<Sonner />
|
||||||
|
|||||||
83
src/components/ui/network-status-banner.tsx
Normal file
83
src/components/ui/network-status-banner.tsx
Normal 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;
|
||||||
|
}
|
||||||
54
src/hooks/useCircuitBreakerStatus.ts
Normal file
54
src/hooks/useCircuitBreakerStatus.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -98,6 +98,13 @@ export class CircuitBreaker {
|
|||||||
logger.info('Circuit breaker CLOSED - service recovered', {
|
logger.info('Circuit breaker CLOSED - service recovered', {
|
||||||
successCount: this.successCount
|
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,
|
monitoringWindow: this.config.monitoringWindow,
|
||||||
resetTimeout: this.config.resetTimeout
|
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
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user