diff --git a/visual_workflow_builder/frontend/src/components/CoachingPanel/CoachingSuggestionCard.tsx b/visual_workflow_builder/frontend/src/components/CoachingPanel/CoachingSuggestionCard.tsx index 2514bda2c..156c6d6ca 100644 --- a/visual_workflow_builder/frontend/src/components/CoachingPanel/CoachingSuggestionCard.tsx +++ b/visual_workflow_builder/frontend/src/components/CoachingPanel/CoachingSuggestionCard.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { CoachingSuggestion } from '../../hooks/useCoachingWebSocket'; +import { getApiOrigin } from '../../services/apiClient'; interface CoachingSuggestionCardProps { suggestion: CoachingSuggestion; @@ -106,7 +107,7 @@ const CoachingSuggestionCard: React.FC = ({ {suggestion.screenshotPath && (
Target element { (e.target as HTMLImageElement).style.display = 'none'; diff --git a/visual_workflow_builder/frontend/src/components/CoachingPanel/index.tsx b/visual_workflow_builder/frontend/src/components/CoachingPanel/index.tsx index ed418176a..8ae025c5e 100644 --- a/visual_workflow_builder/frontend/src/components/CoachingPanel/index.tsx +++ b/visual_workflow_builder/frontend/src/components/CoachingPanel/index.tsx @@ -23,6 +23,7 @@ import CoachingSuggestionCard from './CoachingSuggestionCard'; import CoachingDecisionButtons from './CoachingDecisionButtons'; import CoachingStatsDisplay from './CoachingStatsDisplay'; import CorrectionEditor from './CorrectionEditor'; +import { getApiOrigin } from '../../services/apiClient'; import './CoachingPanel.css'; interface CoachingPanelProps { @@ -143,7 +144,7 @@ export const CoachingPanel: React.FC = ({ const startSession = useCallback( async (wfId: string) => { try { - const response = await fetch(`${serverUrl || 'http://localhost:5001'}/api/executions/coaching`, { + const response = await fetch(`${serverUrl || getApiOrigin()}/api/executions/coaching`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ workflow_id: wfId }), diff --git a/visual_workflow_builder/frontend/src/components/Executor/VWBExecutorExtension.tsx b/visual_workflow_builder/frontend/src/components/Executor/VWBExecutorExtension.tsx index 049146f19..d7bab18af 100644 --- a/visual_workflow_builder/frontend/src/components/Executor/VWBExecutorExtension.tsx +++ b/visual_workflow_builder/frontend/src/components/Executor/VWBExecutorExtension.tsx @@ -62,6 +62,7 @@ import { Variable, } from '../../types'; import { VWBEvidence } from '../../types/evidence'; +import { getApiHost } from '../../services/apiClient'; interface VWBExecutorExtensionProps { workflow: Workflow; @@ -229,11 +230,8 @@ const VWBExecutorExtension: React.FC = ({ setFeedbackLoading(true); try { - // Déterminer l'URL de l'API - const hostname = window.location.hostname; - const apiBase = (hostname === 'localhost' || hostname === '127.0.0.1') - ? 'http://localhost:5001/api' - : `http://${hostname}:5000/api`; + // URL de l'API résolue dynamiquement (compatible IP DGX / accès distant). + const apiBase = getApiHost(); const response = await fetch(`${apiBase}/workflows/${workflow.id}/feedback`, { method: 'POST', diff --git a/visual_workflow_builder/frontend/src/components/VisualSelector/index.tsx b/visual_workflow_builder/frontend/src/components/VisualSelector/index.tsx index 9118d2994..63ef0bb93 100644 --- a/visual_workflow_builder/frontend/src/components/VisualSelector/index.tsx +++ b/visual_workflow_builder/frontend/src/components/VisualSelector/index.tsx @@ -62,6 +62,7 @@ import { captureLibraryService, SavedCapture } from '../../services/captureLibra import { VisualSelection, BoundingBox } from '../../types'; import { screenCaptureService } from '../../services/screenCaptureService'; import { uploadAnchorImage } from '../../services/anchorImageService'; +import { getApiHost } from '../../services/apiClient'; interface VisualSelectorProps { isOpen: boolean; @@ -137,7 +138,7 @@ const VisualSelector: React.FC = ({ useEffect(() => { const loadMonitors = async () => { try { - const response = await fetch('http://localhost:5001/api/real-demo/capture/status'); + const response = await fetch(`${getApiHost()}/real-demo/capture/status`); const data = await response.json(); if (data.success && data.monitors) { setMonitors(data.monitors); @@ -301,7 +302,7 @@ const VisualSelector: React.FC = ({ await new Promise(resolve => setTimeout(resolve, delayMs)); // Utiliser l'API de capture réelle avec le moniteur sélectionné - const response = await fetch('http://localhost:5001/api/real-demo/capture', { + const response = await fetch(`${getApiHost()}/real-demo/capture`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/visual_workflow_builder/frontend/src/hooks/useApiClient.ts b/visual_workflow_builder/frontend/src/hooks/useApiClient.ts index 3b0e4fdc1..746c5da3e 100644 --- a/visual_workflow_builder/frontend/src/hooks/useApiClient.ts +++ b/visual_workflow_builder/frontend/src/hooks/useApiClient.ts @@ -8,7 +8,7 @@ */ import { useState, useCallback, useRef, useEffect, useMemo } from 'react'; -import { apiClient, ApiError, ConnectionState } from '../services/apiClient'; +import { apiClient, ApiError, ConnectionState, getApiHost } from '../services/apiClient'; import { WorkflowApiData } from '../types'; // Types pour les états de requête @@ -215,7 +215,7 @@ export function useConnectionState() { // Vérification DIRECTE au montage (SANS passer par apiClient singleton) const checkOnMount = async () => { try { - const response = await fetch('http://localhost:5001/api/health', { + const response = await fetch(`${getApiHost()}/health`, { headers: { 'Accept': 'application/json' }, }); diff --git a/visual_workflow_builder/frontend/src/hooks/useCoachingWebSocket.ts b/visual_workflow_builder/frontend/src/hooks/useCoachingWebSocket.ts index f69047b23..7b8d9965c 100644 --- a/visual_workflow_builder/frontend/src/hooks/useCoachingWebSocket.ts +++ b/visual_workflow_builder/frontend/src/hooks/useCoachingWebSocket.ts @@ -10,6 +10,7 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { io, Socket } from 'socket.io-client'; +import { getApiOrigin } from '../services/apiClient'; // Types for COACHING mode export type CoachingDecision = 'accept' | 'reject' | 'correct' | 'manual' | 'skip'; @@ -106,7 +107,7 @@ const convertStats = (backendStats: Record): CoachingStats => { export function useCoachingWebSocket( options: UseCoachingWebSocketOptions = {} ): UseCoachingWebSocketReturn { - const { serverUrl = 'http://localhost:5001', autoConnect = true } = options; + const { serverUrl = getApiOrigin(), autoConnect = true } = options; const [isConnected, setIsConnected] = useState(false); const [isSubscribed, setIsSubscribed] = useState(false); diff --git a/visual_workflow_builder/frontend/src/hooks/useConnectionStatus.ts b/visual_workflow_builder/frontend/src/hooks/useConnectionStatus.ts index 2c806b17b..88fcbec0c 100644 --- a/visual_workflow_builder/frontend/src/hooks/useConnectionStatus.ts +++ b/visual_workflow_builder/frontend/src/hooks/useConnectionStatus.ts @@ -11,7 +11,7 @@ */ import { useState, useEffect, useCallback, useRef } from 'react'; -import { apiClient, ConnectionState } from '../services/apiClient'; +import { apiClient, ConnectionState, getApiHost } from '../services/apiClient'; interface ConnectionStatusState { /** État actuel de la connexion */ @@ -130,7 +130,7 @@ export function useConnectionStatus(options: UseConnectionStatusOptions = {}): C // Vérification DIRECTE au démarrage const checkOnMount = async () => { try { - const response = await fetch('http://localhost:5001/api/health', { + const response = await fetch(`${getApiHost()}/health`, { headers: { 'Accept': 'application/json' }, }); diff --git a/visual_workflow_builder/frontend/src/hooks/useCorrectionPacks.ts b/visual_workflow_builder/frontend/src/hooks/useCorrectionPacks.ts index 8ec9eb268..21f31c3ff 100644 --- a/visual_workflow_builder/frontend/src/hooks/useCorrectionPacks.ts +++ b/visual_workflow_builder/frontend/src/hooks/useCorrectionPacks.ts @@ -5,6 +5,7 @@ */ import { useState, useCallback, useEffect } from 'react'; +import { getApiHost } from '../services/apiClient'; // Types export interface Correction { @@ -68,7 +69,8 @@ interface UseCorrectionPacksReturn { selectPack: (pack: CorrectionPack | null) => void; } -const API_BASE = 'http://localhost:5001/api'; +// Base URL de l'API résolue dynamiquement (compatible IP DGX / accès distant). +const API_BASE = getApiHost(); export function useCorrectionPacks(): UseCorrectionPacksReturn { const [packs, setPacks] = useState([]); diff --git a/visual_workflow_builder/frontend/src/services/VisualCaptureService.ts b/visual_workflow_builder/frontend/src/services/VisualCaptureService.ts index b943d9814..0537386f9 100644 --- a/visual_workflow_builder/frontend/src/services/VisualCaptureService.ts +++ b/visual_workflow_builder/frontend/src/services/VisualCaptureService.ts @@ -9,6 +9,16 @@ import { BoundingBox } from '../types'; +// Origine de l'API core (port 8000) résolue dynamiquement à partir de l'hôte courant. +// NOTE: le port 8000 (API core upload) est conservé tel quel ; seul l'hôte devient +// dynamique pour rester compatible avec un accès distant (IP DGX). +const getCoreApiOrigin = (): string => { + if (typeof window !== 'undefined') { + return `http://${window.location.hostname}:8000`; + } + return (process.env.REACT_APP_CORE_API_ORIGIN || '').replace(/\/$/, ''); +}; + interface VisualMetadata { element_type: string; relative_position?: string; @@ -63,7 +73,7 @@ class VisualCaptureService { private cache: Map; private cacheTimeout: number; - constructor(baseUrl: string = 'http://localhost:8000') { + constructor(baseUrl: string = getCoreApiOrigin()) { this.baseUrl = baseUrl; this.timeout = 30000; // 30 secondes this.cache = new Map(); @@ -527,4 +537,4 @@ class VisualCaptureService { // Instance singleton du service export const visualCaptureService = new VisualCaptureService(); -export default VisualCaptureService; \ No newline at end of file +export default VisualCaptureService; diff --git a/visual_workflow_builder/frontend/src/services/anchorImageService.ts b/visual_workflow_builder/frontend/src/services/anchorImageService.ts index 4c921e885..a19f1159e 100644 --- a/visual_workflow_builder/frontend/src/services/anchorImageService.ts +++ b/visual_workflow_builder/frontend/src/services/anchorImageService.ts @@ -8,8 +8,28 @@ */ import { BoundingBox } from '../types'; +import { getApiOrigin } from './apiClient'; -const API_BASE = 'http://localhost:5001'; +// Origine de l'API résolue dynamiquement (compatible IP DGX / accès distant). +// Calculée à l'appel pour refléter window.location au runtime. +const apiBase = (): string => getApiOrigin(); + +/** + * Normalise une URL d'ancre potentiellement importée d'un autre poste. + * + * Les workflows importés peuvent contenir des URLs absolues codées sur une + * ancienne origine applicative. On réécrit + * uniquement le chemin /api/anchor-images vers l'origine API courante, sans + * muter le workflow source (transformation à l'usage uniquement). + */ +const normalizeAnchorUrl = (url: string): string => { + const marker = '/api/anchor-images'; + const idx = url.indexOf(marker); + if (idx === -1) { + return url; + } + return `${apiBase()}${url.slice(idx)}`; +}; export interface AnchorImageUploadResult { success: boolean; @@ -67,7 +87,7 @@ export async function uploadAnchorImage( anchorId }); - const response = await fetch(`${API_BASE}/api/anchor-images`, { + const response = await fetch(`${apiBase()}/api/anchor-images`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -106,7 +126,7 @@ export async function uploadAnchorImage( * @returns URL complète de la miniature */ export function getThumbnailUrl(anchorId: string): string { - return `${API_BASE}/api/anchor-images/${anchorId}/thumbnail`; + return `${apiBase()}/api/anchor-images/${anchorId}/thumbnail`; } /** @@ -116,7 +136,7 @@ export function getThumbnailUrl(anchorId: string): string { * @returns URL complète de l'image originale */ export function getOriginalUrl(anchorId: string): string { - return `${API_BASE}/api/anchor-images/${anchorId}/original`; + return `${apiBase()}/api/anchor-images/${anchorId}/original`; } /** @@ -126,7 +146,7 @@ export function getOriginalUrl(anchorId: string): string { * @returns Métadonnées de l'ancre */ export async function getAnchorMetadata(anchorId: string): Promise { - const response = await fetch(`${API_BASE}/api/anchor-images/${anchorId}/metadata`); + const response = await fetch(`${apiBase()}/api/anchor-images/${anchorId}/metadata`); if (!response.ok) { throw new Error(`Ancre '${anchorId}' non trouvée`); @@ -143,7 +163,7 @@ export async function getAnchorMetadata(anchorId: string): Promise { - const response = await fetch(`${API_BASE}/api/anchor-images/${anchorId}`, { + const response = await fetch(`${apiBase()}/api/anchor-images/${anchorId}`, { method: 'DELETE', }); @@ -166,7 +186,7 @@ export async function listAnchorImages( offset: number = 0 ): Promise<{ anchors: AnchorMetadata[]; total: number }> { const response = await fetch( - `${API_BASE}/api/anchor-images?limit=${limit}&offset=${offset}` + `${apiBase()}/api/anchor-images?limit=${limit}&offset=${offset}` ); if (!response.ok) { @@ -186,7 +206,7 @@ export async function listAnchorImages( * @returns Statistiques de stockage */ export async function getStorageStats(): Promise { - const response = await fetch(`${API_BASE}/api/anchor-images/stats`); + const response = await fetch(`${apiBase()}/api/anchor-images/stats`); if (!response.ok) { throw new Error('Erreur lors de la récupération des statistiques'); @@ -205,7 +225,7 @@ export async function getStorageStats(): Promise { export async function anchorExists(anchorId: string): Promise { try { const response = await fetch( - `${API_BASE}/api/anchor-images/${anchorId}/metadata`, + `${apiBase()}/api/anchor-images/${anchorId}/metadata`, { method: 'HEAD' } ); return response.ok; @@ -229,17 +249,18 @@ export function getPreviewImageUrl(anchor: { }): string | null { // Priorité 1: URL de miniature serveur if (anchor.thumbnail_url) { - // Si l'URL est relative, ajouter le préfixe API + // URL absolue: normaliser une éventuelle origine périmée (workflow importé). + // URL relative: préfixer avec l'origine API courante. return anchor.thumbnail_url.startsWith('http') - ? anchor.thumbnail_url - : `${API_BASE}${anchor.thumbnail_url}`; + ? normalizeAnchorUrl(anchor.thumbnail_url) + : `${apiBase()}${anchor.thumbnail_url}`; } // Priorité 2: URL d'image originale serveur if (anchor.reference_image_url) { return anchor.reference_image_url.startsWith('http') - ? anchor.reference_image_url - : `${API_BASE}${anchor.reference_image_url}`; + ? normalizeAnchorUrl(anchor.reference_image_url) + : `${apiBase()}${anchor.reference_image_url}`; } // Priorité 3: Construire l'URL depuis anchor_id si présent diff --git a/visual_workflow_builder/frontend/src/services/apiClient.ts b/visual_workflow_builder/frontend/src/services/apiClient.ts index 2cc0a3ce6..1c069b900 100644 --- a/visual_workflow_builder/frontend/src/services/apiClient.ts +++ b/visual_workflow_builder/frontend/src/services/apiClient.ts @@ -46,20 +46,33 @@ type ConnectionState = 'online' | 'offline' | 'checking'; // Callbacks pour les changements d'état type ConnectionStateCallback = (state: ConnectionState) => void; -// Détection automatique de l'hôte pour support multi-machines -// Si on accède via une IP (ex: 192.168.1.40), utiliser cette IP pour l'API -// Sinon utiliser localhost -const getApiHost = (): string => { +// Détection automatique de l'hôte pour support multi-machines. +// Si on accède via une IP ou un nom DNS DGX, utiliser cet hôte pour l'API. +// +// IMPORTANT: le backend Flask (dashboard/API VWB) écoute sur le port 5001. +// On résout dynamiquement l'origine à partir de window.location.hostname pour +// rester compatible avec un accès distant (IP DGX) sans URL codée en dur. + +/** + * Origine de l'API (sans suffixe /api), résolue dynamiquement. + * Exemple: http://192.168.1.45:5001 ou http://dgx-site:5001 + */ +export const getApiOrigin = (): string => { if (typeof window !== 'undefined') { const hostname = window.location.hostname; - // Si c'est localhost ou 127.0.0.1, garder localhost - if (hostname === 'localhost' || hostname === '127.0.0.1') { - return 'http://localhost:5001/api'; - } - // Sinon utiliser le même hostname (IP) avec le port 5000 - return `http://${hostname}:5000/api`; + // En accès distant comme en local, le backend Flask reste sur le port 5001 + return `http://${hostname}:5001`; } - return 'http://localhost:5001/api'; + return (process.env.REACT_APP_API_ORIGIN || '').replace(/\/$/, ''); +}; + +/** + * URL de base de l'API (avec suffixe /api), résolue dynamiquement. + * Exemple: http://192.168.1.45:5001/api ou http://dgx-site:5001/api + */ +export const getApiHost = (): string => { + const origin = getApiOrigin(); + return origin ? `${origin}/api` : '/api'; }; // Configuration par défaut diff --git a/visual_workflow_builder/frontend/src/services/catalogService.ts b/visual_workflow_builder/frontend/src/services/catalogService.ts index 7bdf9d5a3..b2f1aa37f 100644 --- a/visual_workflow_builder/frontend/src/services/catalogService.ts +++ b/visual_workflow_builder/frontend/src/services/catalogService.ts @@ -46,6 +46,9 @@ import { getStaticCatalogStats, } from '../data/staticCatalog'; +// Origine de l'API résolue dynamiquement (compatible IP DGX / accès distant). +import { getApiOrigin } from './apiClient'; + // Configuration du service catalogue interface CatalogServiceConfig { urls: string[]; @@ -173,18 +176,17 @@ class CatalogService { const currentOrigin = window.location.origin; candidateUrls.push(currentOrigin); - // 4. Localhost standard (développement) - Port 5001 en priorité - if (!candidateUrls.includes('http://localhost:5001')) { - candidateUrls.push('http://localhost:5001'); - } - if (!candidateUrls.includes('http://localhost:5001')) { - candidateUrls.push('http://localhost:5001'); + // 4. Origine API courante (port 5001), résolue dynamiquement. + // En accès navigateur -> http://:5001 + const apiOrigin = getApiOrigin(); + if (!candidateUrls.includes(apiOrigin)) { + candidateUrls.push(apiOrigin); } // 5. IP locale détectée (cross-machine) try { const localIp = this.detectLocalIp(); - if (localIp && localIp !== '127.0.0.1') { + if (localIp && !localIp.startsWith('127.')) { candidateUrls.push(`http://${localIp}:5000`); candidateUrls.push(`http://${localIp}:5004`); } @@ -973,4 +975,4 @@ export type { CatalogActionCategory, }; -export default CatalogService; \ No newline at end of file +export default CatalogService; diff --git a/visual_workflow_builder/frontend/src/services/evidenceService.ts b/visual_workflow_builder/frontend/src/services/evidenceService.ts index 369fd27c0..c9f7e5b41 100644 --- a/visual_workflow_builder/frontend/src/services/evidenceService.ts +++ b/visual_workflow_builder/frontend/src/services/evidenceService.ts @@ -4,13 +4,15 @@ */ import { VWBEvidence, EvidenceFilters, EvidenceExportOptions, EvidenceStats, EvidenceUtils } from '../types/evidence'; +import { getApiOrigin } from './apiClient'; export class EvidenceService { private baseUrl: string; private cache: Map = new Map(); private cacheTimeout: number = 5 * 60 * 1000; // 5 minutes - constructor(baseUrl: string = 'http://localhost:5001') { + // baseUrl résolu dynamiquement par défaut (origine API courante, compatible IP DGX) + constructor(baseUrl: string = getApiOrigin()) { this.baseUrl = baseUrl; } diff --git a/visual_workflow_builder/frontend/src/services/realScreenCaptureService.ts b/visual_workflow_builder/frontend/src/services/realScreenCaptureService.ts index 1bd2b3dfd..b46380175 100644 --- a/visual_workflow_builder/frontend/src/services/realScreenCaptureService.ts +++ b/visual_workflow_builder/frontend/src/services/realScreenCaptureService.ts @@ -6,8 +6,11 @@ * en utilisant le service RealScreenCaptureService du backend. */ +import { getApiHost } from './apiClient'; + // Configuration du service -const BACKEND_BASE_URL = 'http://localhost:5001/api'; +// Base URL de l'API résolue dynamiquement (compatible IP DGX / accès distant). +const BACKEND_BASE_URL = getApiHost(); const REQUEST_TIMEOUT = 20000; // 20 secondes pour la capture avec détection // Types pour les réponses API diff --git a/visual_workflow_builder/frontend/src/services/screenCaptureService.ts b/visual_workflow_builder/frontend/src/services/screenCaptureService.ts index 8b59ff86f..835c6acd8 100644 --- a/visual_workflow_builder/frontend/src/services/screenCaptureService.ts +++ b/visual_workflow_builder/frontend/src/services/screenCaptureService.ts @@ -7,9 +7,11 @@ */ import { BoundingBox, VisualSelection } from '../types'; +import { getApiHost } from './apiClient'; // Configuration du service -const BACKEND_BASE_URL = 'http://localhost:5001/api'; +// Base URL de l'API résolue dynamiquement (compatible IP DGX / accès distant). +const BACKEND_BASE_URL = getApiHost(); const REQUEST_TIMEOUT = 15000; // 15 secondes pour la capture d'écran // Types pour les réponses API