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
2026-01-18 15:28:22 +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:updateQueueSize',message:'Queue size updated',data:{queueSize:size,isOnline},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'C'})}).catch(()=>{});
2026-01-18 12:55:28 +00:00
// #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
2026-01-18 15:28:22 +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:useEffect',message:'Sync bar effect triggered',data:{isProcessing,queueSize,isOnline,shouldShowSyncBar},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'B'})}).catch(()=>{});
2026-01-18 12:55:28 +00:00
// #endregion
if ( isProcessing && queueSize > 0 && isOnline ) {
const timer = setTimeout ( ( ) = > {
// #region agent log
2026-01-18 15:28:22 +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:setTimeout',message:'Setting shouldShowSyncBar to true',data:{queueSize,isProcessing},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'B'})}).catch(()=>{});
2026-01-18 12:55:28 +00:00
// #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 15:28:22 +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(()=>{});
2026-01-18 12:55:28 +00:00
}
// #endregion
if ( isProcessing && queueSize > 0 && shouldShowSyncBar ) {
2026-01-11 15:30:43 +00:00
return (
2026-01-15 17:02:40 +00:00
< >
ui(tokens): migrate border-kodo-steel to border-border (86 files, 269 instances)
Replace legacy hardcoded border-kodo-steel (RGB 59,69,84, theme-unaware)
with semantic border-border token across 86 user-facing components.
Covers UI primitives (checkbox, badge, modal, table, textarea, alert,
radio-group, avatar), all modals, settings views, social features,
playlist views, inventory, chat, commerce, and cloud file browser.
Only story/test files retain the legacy token.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 23:07:00 +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-border" >
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
2026-01-18 15:28:22 +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:clearQueue',message:'User clicked clear queue',data:{queueSize},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'C'})}).catch(()=>{});
2026-01-18 12:55:28 +00:00
// #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
}