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>
597 lines
15 KiB
Markdown
597 lines
15 KiB
Markdown
# 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. |