2026-01-11 15:30:43 +00:00
import { useEffect , useState } from 'react' ;
2025-12-03 21:56:50 +00:00
import { useOnlineStatus } from '@/hooks/useOnlineStatus' ;
2026-01-11 15:30:43 +00:00
import { offlineQueue } from '@/services/offlineQueue' ;
2026-01-15 17:02:40 +00:00
import { WifiOff , Loader2 , List } from 'lucide-react' ;
2026-01-11 16:16:49 +00:00
import { hasRecentNetworkError } from '@/utils/networkErrorTracker' ;
2026-01-15 17:02:40 +00:00
import { OfflineQueueManager } from './OfflineQueueManager' ;
2025-12-03 21:56:50 +00:00
/ * *
2026-01-11 15:30:43 +00:00
* Composant pour afficher un indicateur de mode hors ligne avec nombre de requêtes en attente
2025-12-03 21:56:50 +00:00
* /
export function OfflineIndicator() {
const isOnline = useOnlineStatus ( ) ;
2026-01-11 15:30:43 +00:00
const [ queueSize , setQueueSize ] = useState ( 0 ) ;
const [ isProcessing , setIsProcessing ] = useState ( false ) ;
2026-01-11 16:16:49 +00:00
const [ hasNetworkError , setHasNetworkError ] = useState ( false ) ;
2026-01-15 17:02:40 +00:00
const [ showQueueManager , setShowQueueManager ] = useState ( false ) ;
2026-01-18 12:55:28 +00:00
const [ shouldShowSyncBar , setShouldShowSyncBar ] = useState ( false ) ;
2025-12-03 21:56:50 +00:00
2026-01-11 15:30:43 +00:00
// Mettre à jour la taille de la file d'attente
useEffect ( ( ) = > {
const updateQueueSize = ( ) = > {
2026-01-18 12:55:28 +00:00
const size = offlineQueue . getQueueSize ( ) ;
// #region agent log
fetch ( 'http://127.0.0.1:7242/ingest/09c5ea5e-2380-4cc3-92aa-d26f3b3d26f6' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { location : 'OfflineIndicator.tsx:updateQueueSize' , message : 'Queue size updated' , data : { queueSize :size , isOnline } , timestamp :Date.now ( ) , sessionId : 'debug-session' , runId : 'run1' , hypothesisId : 'C' } ) } ) . catch ( ( ) = > { } ) ;
// #endregion
setQueueSize ( size ) ;
2026-01-11 15:30:43 +00:00
} ;
// Mettre à jour immédiatement
updateQueueSize ( ) ;
// Mettre à jour toutes les secondes
const interval = setInterval ( updateQueueSize , 1000 ) ;
return ( ) = > clearInterval ( interval ) ;
} , [ ] ) ;
// Vérifier si la file est en cours de traitement
useEffect ( ( ) = > {
if ( isOnline && queueSize > 0 ) {
setIsProcessing ( true ) ;
// Vérifier périodiquement si le traitement est terminé
const checkProcessing = setInterval ( ( ) = > {
const currentSize = offlineQueue . getQueueSize ( ) ;
if ( currentSize === 0 ) {
setIsProcessing ( false ) ;
clearInterval ( checkProcessing ) ;
}
} , 500 ) ;
return ( ) = > clearInterval ( checkProcessing ) ;
} else {
setIsProcessing ( false ) ;
2026-01-11 16:16:49 +00:00
return undefined ;
2026-01-11 15:30:43 +00:00
}
} , [ isOnline , queueSize ] ) ;
2026-01-11 16:16:49 +00:00
// Check for recent network errors
useEffect ( ( ) = > {
const checkNetworkError = ( ) = > {
setHasNetworkError ( hasRecentNetworkError ( ) ) ;
} ;
// Check immediately
checkNetworkError ( ) ;
// Check periodically (every 2 seconds) to update when error expires
const interval = setInterval ( checkNetworkError , 2000 ) ;
return ( ) = > clearInterval ( interval ) ;
} , [ ] ) ;
2026-01-18 12:55:28 +00:00
// FIX: Délai avant d'afficher la barre de synchronisation pour éviter les flashs
useEffect ( ( ) = > {
// #region agent log
fetch ( 'http://127.0.0.1:7242/ingest/09c5ea5e-2380-4cc3-92aa-d26f3b3d26f6' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { location : 'OfflineIndicator.tsx:useEffect' , message : 'Sync bar effect triggered' , data : { isProcessing , queueSize , isOnline , shouldShowSyncBar } , timestamp :Date.now ( ) , sessionId : 'debug-session' , runId : 'run1' , hypothesisId : 'B' } ) } ) . catch ( ( ) = > { } ) ;
// #endregion
if ( isProcessing && queueSize > 0 && isOnline ) {
const timer = setTimeout ( ( ) = > {
// #region agent log
fetch ( 'http://127.0.0.1:7242/ingest/09c5ea5e-2380-4cc3-92aa-d26f3b3d26f6' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { location : 'OfflineIndicator.tsx:setTimeout' , message : 'Setting shouldShowSyncBar to true' , data : { queueSize , isProcessing } , timestamp :Date.now ( ) , sessionId : 'debug-session' , runId : 'run1' , hypothesisId : 'B' } ) } ) . catch ( ( ) = > { } ) ;
// #endregion
setShouldShowSyncBar ( true ) ;
} , 500 ) ;
return ( ) = > {
clearTimeout ( timer ) ;
setShouldShowSyncBar ( false ) ;
} ;
} else {
setShouldShowSyncBar ( false ) ;
return undefined ;
}
} , [ isProcessing , queueSize , isOnline ] ) ;
2026-01-11 16:16:49 +00:00
// Ne rien afficher si en ligne, aucune requête en attente, et pas d'erreur réseau récente
if ( isOnline && queueSize === 0 && ! isProcessing && ! hasNetworkError ) {
2026-01-11 15:30:43 +00:00
return null ;
}
2026-01-11 16:16:49 +00:00
// Mode hors ligne ou erreur réseau récente
if ( ! isOnline || hasNetworkError ) {
2026-01-11 15:30:43 +00:00
return (
2026-01-15 17:02:40 +00:00
< >
< div className = "fixed top-0 left-0 right-0 bg-kodo-red/90 backdrop-blur-sm text-white px-4 py-2.5 text-sm z-50 flex items-center justify-center gap-2 shadow-lg border-b border-kodo-red" >
< WifiOff className = "w-4 h-4" / >
< span >
Mode hors ligne
{ queueSize > 0 && (
< span className = "ml-2 font-semibold" >
- { queueSize } { queueSize === 1 ? 'requête' : 'requêtes' } en
attente
< / span >
) }
< / span >
2026-01-11 15:30:43 +00:00
{ queueSize > 0 && (
2026-01-15 17:02:40 +00:00
< button
onClick = { ( ) = > setShowQueueManager ( true ) }
className = "ml-3 px-2 py-1 bg-white/10 hover:bg-white/20 rounded border border-white/20 transition-colors flex items-center gap-1.5 text-xs font-medium"
title = "View queued requests"
>
< List className = "w-3.5 h-3.5" / >
View Queue
< / button >
2026-01-11 15:30:43 +00:00
) }
2026-01-15 17:02:40 +00:00
< / div >
< OfflineQueueManager
open = { showQueueManager }
onClose = { ( ) = > setShowQueueManager ( false ) }
/ >
< / >
2026-01-11 15:30:43 +00:00
) ;
}
// En ligne mais traitement de la file en cours
2026-01-18 12:55:28 +00:00
// FIX: Ne pas afficher la barre si les requêtes sont rapidement traitées (moins de 500ms)
// Cela évite d'afficher la barre pour des requêtes qui sont déjà en cours de traitement
// #region agent log
2026-01-11 15:30:43 +00:00
if ( isProcessing && queueSize > 0 ) {
2026-01-18 12:55:28 +00:00
fetch ( 'http://127.0.0.1:7242/ingest/09c5ea5e-2380-4cc3-92aa-d26f3b3d26f6' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { location : 'OfflineIndicator.tsx:123' , message : 'Checking sync bar display' , data : { isProcessing , queueSize , shouldShowSyncBar , willShow :shouldShowSyncBar } , timestamp :Date.now ( ) , sessionId : 'debug-session' , runId : 'run1' , hypothesisId : 'B' } ) } ) . catch ( ( ) = > { } ) ;
}
// #endregion
if ( isProcessing && queueSize > 0 && shouldShowSyncBar ) {
2026-01-11 15:30:43 +00:00
return (
2026-01-15 17:02:40 +00:00
< >
2026-01-16 10:40:13 +00:00
< div className = "fixed top-0 left-0 right-0 bg-kodo-cyan/90 backdrop-blur-sm text-kodo-void px-4 py-2.5 text-sm z-50 flex items-center justify-center gap-2 shadow-lg border-b border-kodo-steel" >
2026-01-15 17:02:40 +00:00
< Loader2 className = "w-4 h-4 animate-spin" / >
< span >
Synchronisation en cours
{ queueSize > 0 && (
< span className = "ml-2 font-semibold" >
- { queueSize } { queueSize === 1 ? 'requête' : 'requêtes' } restante
{ queueSize > 1 ? 's' : '' }
< / span >
) }
< / span >
2026-01-11 15:30:43 +00:00
{ queueSize > 0 && (
2026-01-18 12:55:28 +00:00
< >
< button
onClick = { async ( ) = > {
// #region agent log
fetch ( 'http://127.0.0.1:7242/ingest/09c5ea5e-2380-4cc3-92aa-d26f3b3d26f6' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { location : 'OfflineIndicator.tsx:clearQueue' , message : 'User clicked clear queue' , data : { queueSize } , timestamp :Date.now ( ) , sessionId : 'debug-session' , runId : 'run1' , hypothesisId : 'C' } ) } ) . catch ( ( ) = > { } ) ;
// #endregion
await offlineQueue . clearQueue ( ) ;
setQueueSize ( 0 ) ;
} }
className = "ml-2 px-2 py-1 bg-kodo-red/20 hover:bg-kodo-red/30 rounded border border-kodo-red/30 transition-colors flex items-center gap-1.5 text-xs font-medium"
title = "Clear queued requests"
>
Clear Queue
< / button >
< button
onClick = { ( ) = > setShowQueueManager ( true ) }
className = "ml-2 px-2 py-1 bg-kodo-void/20 hover:bg-kodo-void/30 rounded border border-kodo-void/30 transition-colors flex items-center gap-1.5 text-xs font-medium"
title = "View queued requests"
>
< List className = "w-3.5 h-3.5" / >
View Queue
< / button >
< / >
2026-01-11 15:30:43 +00:00
) }
2026-01-15 17:02:40 +00:00
< / div >
< OfflineQueueManager
open = { showQueueManager }
onClose = { ( ) = > setShowQueueManager ( false ) }
/ >
< / >
2026-01-11 15:30:43 +00:00
) ;
}
return null ;
2025-12-03 21:56:50 +00:00
}