feat: replay E2E fonctionnel — 25/25 actions, 0 retries, SomEngine via serveur

Validé sur PC Windows (DESKTOP-58D5CAC, 2560x1600) :
- 8 clics résolus visuellement (1 anchor_template, 1 som_text_match, 6 som_vlm)
- Score moyen 0.75, temps moyen 1.6s
- Texte tapé correctement (bonjour, test word, date, email)
- 0 retries, 2 actions non vérifiées (OK)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-03-31 14:04:41 +02:00
parent 5e0b53cfd1
commit a7de6a488b
79542 changed files with 6091757 additions and 1 deletions

View File

@@ -0,0 +1,597 @@
# 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<string | null> {
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<boolean> {
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: <OnlineIcon color="success" />,
label: 'En ligne',
color: 'success',
tooltip: `Catalogue en ligne - ${actionCount} actions disponibles`
};
case 'static':
return {
icon: <OfflineIcon color="warning" />,
label: 'Hors ligne',
color: 'warning',
tooltip: `Mode hors ligne - ${actionCount} actions de base disponibles`
};
case 'detecting':
return {
icon: <CircularProgress size={16} />,
label: 'Détection...',
color: 'info',
tooltip: 'Détection du service catalogue en cours...'
};
}
};
const config = getStatusConfig();
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Tooltip title={config.tooltip} placement="left">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
{config.icon}
<Typography variant="caption" color={config.color}>
{config.label}
</Typography>
<Badge badgeContent={actionCount} color="primary" max={99} />
</Box>
</Tooltip>
{error && (
<Tooltip title="Cliquer pour réessayer">
<IconButton size="small" onClick={onRetry}>
<RefreshIcon fontSize="small" />
</IconButton>
</Tooltip>
)}
</Box>
);
};
```
## 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<string | null>;
priority: number;
}
class OriginUrlStrategy implements UrlDetectionStrategy {
priority = 1;
async detect(): Promise<string | null> {
const url = `${window.location.origin}/api/vwb/catalog`;
return await catalogService.testUrl(url) ? url : null;
}
}
class LocalhostStrategy implements UrlDetectionStrategy {
priority = 2;
async detect(): Promise<string | null> {
const url = 'http://localhost:5004/api/vwb/catalog';
return await catalogService.testUrl(url) ? url : null;
}
}
class LocalIPStrategy implements UrlDetectionStrategy {
priority = 3;
async detect(): Promise<string | null> {
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<T> {
data: T;
timestamp: number;
ttl: number;
}
class CatalogCache {
private cache = new Map<string, CacheEntry<any>>();
set<T>(key: string, data: T, ttl: number = 24 * 60 * 60 * 1000) {
this.cache.set(key, {
data,
timestamp: Date.now(),
ttl
});
}
get<T>(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<string, any>;
}
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<string, number>();
private async makeRequest<T>(endpoint: string): Promise<T> {
// 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.