# Design Technique : Résolution du Problème de Palette Vide Cross-Machine ## Vue d'Ensemble Architecturale Cette spécification définit l'architecture technique pour résoudre le problème de palette vide dans le Visual Workflow Builder lorsqu'utilisé sur une machine distante. **Auteur :** Dom, Alice, Kiro - 10 janvier 2026 ## Architecture Actuelle vs Cible ### Architecture Actuelle (Problématique) ``` Frontend (Machine A) → catalogService.ts → http://localhost:5004 (Machine A) ↓ ❌ ÉCHEC (Backend sur Machine B) ↓ Palette Vide ``` ### Architecture Cible (Solution) ``` Frontend (Machine A) → catalogService.ts → Détection Auto URL ↓ ┌─ http://current-origin/api/vwb/catalog (Machine A) ├─ http://localhost:5004 (Machine A) ├─ http://192.168.x.x:5004 (Machine B) └─ Catalogue Statique (Fallback) ↓ ✅ Palette Complète ``` ## Composants Modifiés ### 1. Service Catalogue Étendu #### Interface de Configuration ```typescript interface CatalogServiceConfig { // URLs à tester dans l'ordre candidateUrls: string[]; // Configuration de retry timeout: number; maxRetries: number; retryDelay: number; // Cache de configuration cacheKey: string; cacheDuration: number; // Mode fallback enableStaticFallback: boolean; staticCatalogPath: string; } ``` #### Méthode de Détection d'URL ```typescript class CatalogService { private async detectBackendUrl(): Promise { const candidates = [ // 1. URL courante (même origine) `${window.location.origin}/api/vwb/catalog`, // 2. Configuration explicite process.env.REACT_APP_CATALOG_URL, new URLSearchParams(window.location.search).get('catalogUrl'), // 3. URLs par défaut 'http://localhost:5004/api/vwb/catalog', // 4. IP locale détectée await this.detectLocalIP() + ':5004/api/vwb/catalog' ].filter(Boolean); for (const url of candidates) { if (await this.testUrl(url)) { this.cacheWorkingUrl(url); return url; } } return null; // Déclenche fallback statique } private async testUrl(url: string): Promise { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 2000); const response = await fetch(`${url}/health`, { signal: controller.signal, method: 'GET', headers: { 'Accept': 'application/json' } }); clearTimeout(timeoutId); return response.ok; } catch { return false; } } } ``` ### 2. Catalogue Statique de Secours #### Structure du Catalogue Statique ```typescript // visual_workflow_builder/frontend/src/data/staticCatalog.ts export const STATIC_CATALOG_ACTIONS: VWBCatalogAction[] = [ { id: 'click_anchor', name: 'Cliquer sur Élément', description: 'Cliquer sur un élément identifié visuellement', category: 'vision_ui', icon: '🖱️', parameters: { visual_anchor: { type: 'VWBVisualAnchor', required: true, description: 'Élément à cliquer' }, click_type: { type: 'string', required: false, default: 'left', options: ['left', 'right', 'double'], description: 'Type de clic' } }, examples: [ { name: 'Clic simple sur bouton', description: 'Cliquer sur un bouton "Valider"', parameters: { visual_anchor: { /* anchor data */ }, click_type: 'left' } } ], metadata: { version: '1.0.0', author: 'Dom, Alice, Kiro', complexity: 'simple', estimatedDuration: 1000 } }, // ... autres actions ]; export const STATIC_CATALOG_CATEGORIES: VWBActionCategoryInfo[] = [ { id: 'vision_ui', name: 'Interface Utilisateur', description: 'Actions d\'interaction visuelle', icon: '🖱️', color: '#2196f3', actionCount: 5, isEnabled: true } ]; ``` ### 3. Hook useCatalogActions Étendu #### Logique de Fallback ```typescript export const useCatalogActions = (options: UseCatalogActionsOptions = {}) => { const [mode, setMode] = useState<'dynamic' | 'static' | 'detecting'>('detecting'); const loadCatalogData = useCallback(async () => { setState(prev => ({ ...prev, isLoading: true, error: null })); try { // Tentative de chargement dynamique const dynamicData = await catalogService.getActions(); setState({ actions: dynamicData.actions, categories: await catalogService.getCategories(), isLoading: false, isOnline: true, error: null, lastUpdate: new Date() }); setMode('dynamic'); } catch (error) { console.warn('Catalogue dynamique indisponible, basculement vers statique:', error); // Fallback vers catalogue statique setState({ actions: STATIC_CATALOG_ACTIONS, categories: STATIC_CATALOG_CATEGORIES, isLoading: false, isOnline: false, error: 'Service catalogue indisponible - Mode hors ligne', lastUpdate: new Date() }); setMode('static'); } }, []); // ... reste de l'implémentation }; ``` ### 4. Interface Utilisateur Améliorée #### Indicateurs de Statut dans la Palette ```typescript // Composant StatusIndicator const CatalogStatusIndicator: React.FC<{ mode: 'dynamic' | 'static' | 'detecting'; actionCount: number; error?: string; onRetry: () => void; }> = ({ mode, actionCount, error, onRetry }) => { const getStatusConfig = () => { switch (mode) { case 'dynamic': return { icon: , label: 'En ligne', color: 'success', tooltip: `Catalogue en ligne - ${actionCount} actions disponibles` }; case 'static': return { icon: , label: 'Hors ligne', color: 'warning', tooltip: `Mode hors ligne - ${actionCount} actions de base disponibles` }; case 'detecting': return { icon: , label: 'Détection...', color: 'info', tooltip: 'Détection du service catalogue en cours...' }; } }; const config = getStatusConfig(); return ( {config.icon} {config.label} {error && ( )} ); }; ``` ## Flux de Données ### 1. Séquence de Démarrage ```mermaid sequenceDiagram participant U as Utilisateur participant P as Palette participant H as useCatalogActions participant S as CatalogService participant C as Cache participant SC as StaticCatalog U->>P: Ouvre VWB P->>H: useEffect (autoLoad) H->>S: loadCatalogData() S->>C: getCachedUrl() alt URL en cache valide C-->>S: URL fonctionnelle S->>S: testUrl(cachedUrl) alt URL fonctionne S-->>H: Actions dynamiques H-->>P: État: dynamic, actions else URL échoue S->>S: detectBackendUrl() S-->>H: Nouvelle URL ou null end else Pas de cache S->>S: detectBackendUrl() alt URL trouvée S->>C: cacheWorkingUrl() S-->>H: Actions dynamiques H-->>P: État: dynamic, actions else Aucune URL H->>SC: STATIC_CATALOG_ACTIONS H-->>P: État: static, actions statiques end end ``` ### 2. Gestion des Erreurs Réseau ```mermaid flowchart TD A[Requête Catalogue] --> B{Succès?} B -->|Oui| C[Mode Dynamique] B -->|Non| D[Erreur Réseau] D --> E{Type d'erreur} E -->|Timeout| F[Retry avec délai] E -->|404/500| G[Essayer URL suivante] E -->|Network Error| H[Mode Statique] F --> I{Retry réussi?} I -->|Oui| C I -->|Non| H G --> J{Autres URLs?} J -->|Oui| A J -->|Non| H H --> K[Catalogue Statique] ``` ## Patterns de Design ### 1. Strategy Pattern pour Détection d'URL ```typescript interface UrlDetectionStrategy { detect(): Promise; priority: number; } class OriginUrlStrategy implements UrlDetectionStrategy { priority = 1; async detect(): Promise { const url = `${window.location.origin}/api/vwb/catalog`; return await catalogService.testUrl(url) ? url : null; } } class LocalhostStrategy implements UrlDetectionStrategy { priority = 2; async detect(): Promise { const url = 'http://localhost:5004/api/vwb/catalog'; return await catalogService.testUrl(url) ? url : null; } } class LocalIPStrategy implements UrlDetectionStrategy { priority = 3; async detect(): Promise { const ip = await this.detectLocalIP(); const url = `http://${ip}:5004/api/vwb/catalog`; return await catalogService.testUrl(url) ? url : null; } } ``` ### 2. Observer Pattern pour Statut ```typescript interface CatalogStatusObserver { onStatusChange(status: CatalogStatus): void; } class CatalogService { private observers: CatalogStatusObserver[] = []; addObserver(observer: CatalogStatusObserver) { this.observers.push(observer); } private notifyStatusChange(status: CatalogStatus) { this.observers.forEach(observer => observer.onStatusChange(status)); } } ``` ### 3. Cache Pattern avec Expiration ```typescript interface CacheEntry { data: T; timestamp: number; ttl: number; } class CatalogCache { private cache = new Map>(); set(key: string, data: T, ttl: number = 24 * 60 * 60 * 1000) { this.cache.set(key, { data, timestamp: Date.now(), ttl }); } get(key: string): T | null { const entry = this.cache.get(key); if (!entry) return null; if (Date.now() - entry.timestamp > entry.ttl) { this.cache.delete(key); return null; } return entry.data; } } ``` ## Configuration et Déploiement ### Variables d'Environnement ```bash # .env.example REACT_APP_CATALOG_URL=http://localhost:5004/api/vwb/catalog REACT_APP_CATALOG_TIMEOUT=5000 REACT_APP_CATALOG_RETRY_ATTEMPTS=3 REACT_APP_ENABLE_STATIC_FALLBACK=true ``` ### Configuration Runtime ```typescript // Configuration via URL parameters const urlParams = new URLSearchParams(window.location.search); const catalogConfig = { url: urlParams.get('catalogUrl'), timeout: parseInt(urlParams.get('catalogTimeout') || '5000'), enableFallback: urlParams.get('enableFallback') !== 'false' }; ``` ## Tests et Validation ### Tests Unitaires ```typescript describe('CatalogService URL Detection', () => { it('should try origin URL first', async () => { const service = new CatalogService(); const spy = jest.spyOn(service, 'testUrl'); await service.detectBackendUrl(); expect(spy).toHaveBeenCalledWith( `${window.location.origin}/api/vwb/catalog` ); }); it('should fallback to static catalog when all URLs fail', async () => { const service = new CatalogService(); jest.spyOn(service, 'testUrl').mockResolvedValue(false); const result = await service.detectBackendUrl(); expect(result).toBeNull(); }); }); ``` ### Tests d'Intégration Cross-Machine ```python # tests/integration/test_palette_cross_machine_resolution_10jan2026.py def test_palette_cross_machine_detection(): """Test détection automatique d'URL cross-machine""" # Démarrer backend sur IP différente backend_ip = get_local_ip() start_backend_on_ip(backend_ip, 5004) # Ouvrir frontend sur localhost driver = webdriver.Chrome() driver.get("http://localhost:3000") # Vérifier que la palette se charge palette = wait_for_element(driver, "[data-testid='palette']") actions = palette.find_elements(By.CSS_SELECTOR, "[draggable='true']") # Doit avoir plus que les actions web par défaut assert len(actions) > 4, "Catalogue VisionOnly non chargé" # Vérifier indicateur de statut status = driver.find_element(By.CSS_SELECTOR, "[data-testid='catalog-status']") assert "En ligne" in status.text ``` ## Métriques et Monitoring ### Métriques de Performance ```typescript interface CatalogMetrics { detectionTime: number; successRate: number; fallbackRate: number; cacheHitRate: number; averageResponseTime: number; } class CatalogMetricsCollector { private metrics: CatalogMetrics = { detectionTime: 0, successRate: 0, fallbackRate: 0, cacheHitRate: 0, averageResponseTime: 0 }; recordDetection(startTime: number, success: boolean) { const duration = Date.now() - startTime; this.metrics.detectionTime = duration; // ... mise à jour des métriques } } ``` ### Logging Structuré ```typescript interface CatalogLogEntry { timestamp: string; level: 'info' | 'warn' | 'error'; event: string; data: Record; } class CatalogLogger { log(level: string, event: string, data: any = {}) { const entry: CatalogLogEntry = { timestamp: new Date().toISOString(), level: level as any, event, data }; console.log(`[Catalog] ${entry.timestamp} ${level.toUpperCase()}: ${event}`, data); } } ``` ## Sécurité et Robustesse ### Validation des URLs ```typescript function isValidCatalogUrl(url: string): boolean { try { const parsed = new URL(url); // Protocoles autorisés if (!['http:', 'https:'].includes(parsed.protocol)) { return false; } // Ports autorisés (éviter ports système) const port = parseInt(parsed.port) || (parsed.protocol === 'https:' ? 443 : 80); if (port < 1024 && ![80, 443].includes(port)) { return false; } return true; } catch { return false; } } ``` ### Protection contre les Attaques ```typescript class CatalogService { private rateLimiter = new Map(); private async makeRequest(endpoint: string): Promise { // Rate limiting par URL const now = Date.now(); const lastRequest = this.rateLimiter.get(endpoint) || 0; if (now - lastRequest < 1000) { // Max 1 req/sec par endpoint throw new Error('Rate limit exceeded'); } this.rateLimiter.set(endpoint, now); // ... reste de l'implémentation } } ``` ## Conclusion Cette architecture technique fournit une solution robuste et évolutive pour résoudre le problème de palette vide cross-machine. Elle combine détection automatique intelligente, fallback gracieux, et amélioration de l'expérience utilisateur tout en maintenant la compatibilité avec l'architecture existante du VWB.