fix(vwb): resolve frontend services from runtime host
This commit is contained in:
@@ -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<CoachingSuggestionCardProps> = ({
|
||||
{suggestion.screenshotPath && (
|
||||
<div className="suggestion-screenshot">
|
||||
<img
|
||||
src={`http://localhost:5001${suggestion.screenshotPath}`}
|
||||
src={`${getApiOrigin()}${suggestion.screenshotPath}`}
|
||||
alt="Target element"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
|
||||
@@ -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<CoachingPanelProps> = ({
|
||||
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 }),
|
||||
|
||||
@@ -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<VWBExecutorExtensionProps> = ({
|
||||
|
||||
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',
|
||||
|
||||
@@ -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<VisualSelectorProps> = ({
|
||||
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<VisualSelectorProps> = ({
|
||||
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({
|
||||
|
||||
@@ -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' },
|
||||
});
|
||||
|
||||
|
||||
@@ -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<string, any>): 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);
|
||||
|
||||
@@ -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' },
|
||||
});
|
||||
|
||||
|
||||
@@ -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<CorrectionPack[]>([]);
|
||||
|
||||
@@ -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<string, any>;
|
||||
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;
|
||||
export default VisualCaptureService;
|
||||
|
||||
@@ -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<AnchorMetadata> {
|
||||
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<AnchorMetadat
|
||||
* @returns true si supprimé avec succès
|
||||
*/
|
||||
export async function deleteAnchorImage(anchorId: string): Promise<boolean> {
|
||||
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<StorageStats> {
|
||||
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<StorageStats> {
|
||||
export async function anchorExists(anchorId: string): Promise<boolean> {
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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://<hostname>: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;
|
||||
export default CatalogService;
|
||||
|
||||
@@ -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<string, VWBEvidence[]> = 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user