From e773ca58d16cdde9afbfdbc1ac7fd4b72c1d5da3 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 14:12:23 +0000 Subject: [PATCH] feat: Implement network status banner --- src/App.tsx | 2 + src/components/ui/network-status-banner.tsx | 83 +++++++++++++++++++++ src/hooks/useCircuitBreakerStatus.ts | 54 ++++++++++++++ src/lib/circuitBreaker.ts | 17 +++++ 4 files changed, 156 insertions(+) create mode 100644 src/components/ui/network-status-banner.tsx create mode 100644 src/hooks/useCircuitBreakerStatus.ts diff --git a/src/App.tsx b/src/App.tsx index 82170995..c72a6178 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 { + diff --git a/src/components/ui/network-status-banner.tsx b/src/components/ui/network-status-banner.tsx new file mode 100644 index 00000000..7eda7864 --- /dev/null +++ b/src/components/ui/network-status-banner.tsx @@ -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 ( + + + + Database Unavailable + + + Our systems detected a service outage. Data and authentication are temporarily unavailable. + {' '}Retrying automatically... (next attempt in {countdown}s) + {failureCount > 0 && ( + + {failureCount} failed connection attempts detected + + )} + + + ); + } + + // HALF_OPEN state - testing recovery + if (isHalfOpen) { + return ( + + + + Connection Unstable + + + Testing database connection... Some features may be slow or temporarily unavailable. + + + ); + } + + return null; +} diff --git a/src/hooks/useCircuitBreakerStatus.ts b/src/hooks/useCircuitBreakerStatus.ts new file mode 100644 index 00000000..b645586e --- /dev/null +++ b/src/hooks/useCircuitBreakerStatus.ts @@ -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.CLOSED); + const [failureCount, setFailureCount] = useState(0); + const [lastStateChange, setLastStateChange] = useState(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 + }; +} diff --git a/src/lib/circuitBreaker.ts b/src/lib/circuitBreaker.ts index 8a10d4c0..72f508bf 100644 --- a/src/lib/circuitBreaker.ts +++ b/src/lib/circuitBreaker.ts @@ -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 + } + })); + } } }