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
< >
fix: stabilize builds, tests, and lint across all stacks
Complete stabilization pass bringing all 3 stacks to green:
Frontend (apps/web/):
- Fix TypeScript nullability in useSeason.ts, useTimeOfDay.ts hooks
- Disable no-undef in ESLint config (TypeScript handles it; JSX misidentified)
- Rename 306 story imports from @storybook/react to @storybook/react-vite
- Fix conditional hook call in useMediaQuery.ts useIsTablet
- Move useQuery to top of LoginPage.tsx component
- Remove useless try/catch in GearFormModal.tsx
- Fix stale closure in ResetPasswordPage.tsx handleChange
- Make Storybook decorators (withRouter, withQueryClient, withToast, withAudio)
no-ops since global StorybookDecorator already provides these — prevents
nested Router / duplicate provider crashes in vitest-browser
- Fix nested MemoryRouter in 3 page stories (TrackDetail, PlaylistDetail, UserProfile)
- Update i18n initialization in test setup (await init before changeLanguage)
- Update ~30 test assertions from English to French to match i18n translations
- Update test assertions to match SUMI V3 design changes (shadow vs border)
- Fix remaining story type errors (PlayerError, PlaylistBatchActions,
TrackFilters, VirtualizedChatMessages)
Backend (veza-backend-api/):
- Fix response_test.go RespondWithAppError signature (2 args, not 3)
- Fix TestErrorContractAuthEndpoints expected error codes
(ErrCodeUnauthorized vs ErrCodeInvalidCredentials)
- Fix TestTrackHandler_GetTrackLikes_Success missing auth middleware setup
- Fix TestPlaybackAnalyticsService_GetTrackStats k-anonymity threshold
(needs 5 unique users, not 1)
- Replace NOW() PostgreSQL function with time.Now() parameter in marketplace
service for SQLite test compatibility
- Add missing AutoMigrate entries in marketplace_test.go
(ProductImage, ProductPreview, ProductLicense, ProductReview)
Results:
- Frontend TypeCheck: 617 errors -> 0 errors
- Frontend ESLint: 349 errors -> 0 errors
- Frontend Vitest: 196 failing tests -> 1 skipped (3396/3397 passing)
- Backend go vet: 1 error -> 0 errors
- Backend tests: 5 failing -> all 13 packages passing
- Rust: 150/150 tests passing (unchanged)
- Storybook audit: 0 errors across 1244 stories
Triage report: docs/TRIAGE_REPORT.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:48:07 +00:00
< div className = "fixed top-0 left-0 right-0 bg-destructive/90 backdrop-blur-sm text-foreground px-4 py-2.5 text-sm z-50 flex items-center justify-center gap-2 shadow-[0_4px_12px_rgba(26,26,30,0.15)]" >
2026-01-15 17:02:40 +00:00
< 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
< >
fix: stabilize builds, tests, and lint across all stacks
Complete stabilization pass bringing all 3 stacks to green:
Frontend (apps/web/):
- Fix TypeScript nullability in useSeason.ts, useTimeOfDay.ts hooks
- Disable no-undef in ESLint config (TypeScript handles it; JSX misidentified)
- Rename 306 story imports from @storybook/react to @storybook/react-vite
- Fix conditional hook call in useMediaQuery.ts useIsTablet
- Move useQuery to top of LoginPage.tsx component
- Remove useless try/catch in GearFormModal.tsx
- Fix stale closure in ResetPasswordPage.tsx handleChange
- Make Storybook decorators (withRouter, withQueryClient, withToast, withAudio)
no-ops since global StorybookDecorator already provides these — prevents
nested Router / duplicate provider crashes in vitest-browser
- Fix nested MemoryRouter in 3 page stories (TrackDetail, PlaylistDetail, UserProfile)
- Update i18n initialization in test setup (await init before changeLanguage)
- Update ~30 test assertions from English to French to match i18n translations
- Update test assertions to match SUMI V3 design changes (shadow vs border)
- Fix remaining story type errors (PlayerError, PlaylistBatchActions,
TrackFilters, VirtualizedChatMessages)
Backend (veza-backend-api/):
- Fix response_test.go RespondWithAppError signature (2 args, not 3)
- Fix TestErrorContractAuthEndpoints expected error codes
(ErrCodeUnauthorized vs ErrCodeInvalidCredentials)
- Fix TestTrackHandler_GetTrackLikes_Success missing auth middleware setup
- Fix TestPlaybackAnalyticsService_GetTrackStats k-anonymity threshold
(needs 5 unique users, not 1)
- Replace NOW() PostgreSQL function with time.Now() parameter in marketplace
service for SQLite test compatibility
- Add missing AutoMigrate entries in marketplace_test.go
(ProductImage, ProductPreview, ProductLicense, ProductReview)
Results:
- Frontend TypeCheck: 617 errors -> 0 errors
- Frontend ESLint: 349 errors -> 0 errors
- Frontend Vitest: 196 failing tests -> 1 skipped (3396/3397 passing)
- Backend go vet: 1 error -> 0 errors
- Backend tests: 5 failing -> all 13 packages passing
- Rust: 150/150 tests passing (unchanged)
- Storybook audit: 0 errors across 1244 stories
Triage report: docs/TRIAGE_REPORT.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:48:07 +00:00
< div className = "fixed top-0 left-0 right-0 bg-primary/90 backdrop-blur-sm text-foreground px-4 py-2.5 text-sm z-50 flex items-center justify-center gap-2 shadow-[0_4px_12px_rgba(26,26,30,0.15)]" >
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 ) ;
} }
refactor: Phase 3a — Global color class migration to SUMI semantics
- Replace all kodo-* color classes across ~100 TSX files:
kodo-void → background, kodo-ink → card, kodo-graphite → muted,
kodo-steel → muted-foreground, kodo-cyan → primary, kodo-magenta → destructive,
kodo-lime → success, kodo-red → destructive, kodo-gold → warning
- Replace cyan-500, magenta-500, lime-500 default Tailwind colors with
semantic equivalents (primary, destructive, success)
- Fix WaveformVisualizer hardcoded hex colors to SUMI values
- Delete global-effects.css (conflicting, redundant with index.css)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 00:51:49 +00:00
className = "ml-2 px-2 py-1 bg-destructive/20 hover:bg-destructive/30 rounded border border-destructive/30 transition-colors flex items-center gap-1.5 text-xs font-medium"
2026-01-18 12:55:28 +00:00
title = "Clear queued requests"
>
Clear Queue
< / button >
< button
onClick = { ( ) = > setShowQueueManager ( true ) }
refactor: Phase 3a — Global color class migration to SUMI semantics
- Replace all kodo-* color classes across ~100 TSX files:
kodo-void → background, kodo-ink → card, kodo-graphite → muted,
kodo-steel → muted-foreground, kodo-cyan → primary, kodo-magenta → destructive,
kodo-lime → success, kodo-red → destructive, kodo-gold → warning
- Replace cyan-500, magenta-500, lime-500 default Tailwind colors with
semantic equivalents (primary, destructive, success)
- Fix WaveformVisualizer hardcoded hex colors to SUMI values
- Delete global-effects.css (conflicting, redundant with index.css)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 00:51:49 +00:00
className = "ml-2 px-2 py-1 bg-background/20 hover:bg-background/30 rounded border border-border/30 transition-colors flex items-center gap-1.5 text-xs font-medium"
2026-01-18 12:55:28 +00:00
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
}