2 Commits

Author SHA1 Message Date
Dom
9605cc9d95 fix(vwb): resolve frontend services from runtime host
Some checks failed
tests / Lint (ruff + black) (push) Failing after 1m46s
tests / Tests unitaires (sans GPU) (push) Failing after 1m50s
tests / Tests sécurité (critique) (push) Has been skipped
2026-06-17 17:53:57 +02:00
Dom
667575c3ad feat(installer): make Lea autonomous for POC 2026-06-17 17:53:46 +02:00
20 changed files with 185 additions and 113 deletions

View File

@@ -27,7 +27,7 @@ if platform.system() == "Windows":
except Exception: except Exception:
pass pass
AGENT_VERSION = "1.0.0" AGENT_VERSION = "1.0.1"
# Identifiant unique de la machine (utilisé pour le multi-machine) # Identifiant unique de la machine (utilisé pour le multi-machine)
# Configurable via variable d'environnement, sinon auto-généré depuis hostname + OS # Configurable via variable d'environnement, sinon auto-généré depuis hostname + OS

View File

@@ -53,7 +53,7 @@ AIVANOV ne pourra etre tenu responsable d'un usage non conforme.
7. CONTACT 7. CONTACT
---------- ----------
Pour toute question ou demande d'acces/rectification/suppression Pour toute question ou demande d'acces/rectification/suppression
de donnees : dpo@aivanov.com de donnees : dpo@aivanov.eu
============================================================ ============================================================
En cliquant sur "J'accepte", vous confirmez avoir pris connaissance En cliquant sur "J'accepte", vous confirmez avoir pris connaissance

View File

@@ -23,7 +23,7 @@
; ============================================================ ; ============================================================
#define MyAppName "Lea" #define MyAppName "Lea"
#define MyAppVersion "1.0.0" #define MyAppVersion "1.0.1"
#define MyAppPublisher "AIVANOV" #define MyAppPublisher "AIVANOV"
#define MyAppURL "https://lea.labs.laurinebazin.design" #define MyAppURL "https://lea.labs.laurinebazin.design"
#define MyAppExeName "Lea.bat" #define MyAppExeName "Lea.bat"
@@ -89,24 +89,23 @@ Name: "french"; MessagesFile: "compiler:Languages\French.isl"
[Files] [Files]
; Package complet (code Python + .bat + requirements) ; Package complet (code Python + .bat + requirements)
; Note : install.bat EST copie (execute par [Run] pour creer le venv Python) ; Note : install.bat est EXCLU du staging (runtime 100% embedded, plus de venv/pip)
; Note : config.txt n'est PAS copie depuis le staging (il est genere par [Code]) ; Note : config.txt n'est PAS copie depuis le staging (il est genere par [Code])
Source: "{#SourceDir}\*"; \ Source: "{#SourceDir}\*"; \
DestDir: "{app}"; \ DestDir: "{app}"; \
Flags: ignoreversion recursesubdirs createallsubdirs; \ Flags: ignoreversion recursesubdirs createallsubdirs; \
Excludes: "{#PythonEmbedDir}\*,config.txt,*.log,sessions\*,__pycache__\*" Excludes: "{#PythonEmbedDir}\*,config.txt,*.log,sessions\*,__pycache__\*"
; Python 3.12 embedded (optionnel, copie conditionnelle via check) ; Python 3.12 embedded (OBLIGATOIRE — runtime 100% autonome, aucune dependance Python systeme)
Source: "{#SourceDir}\{#PythonEmbedDir}\*"; \ Source: "{#SourceDir}\{#PythonEmbedDir}\*"; \
DestDir: "{app}\python-embed"; \ DestDir: "{app}\python-embed"; \
Flags: ignoreversion recursesubdirs createallsubdirs skipifsourcedoesntexist; \ Flags: ignoreversion recursesubdirs createallsubdirs
Components: pythonembed
; Script de desinstallation custom (kill + export logs) ; Script de desinstallation custom (kill + export logs)
Source: "uninstall_lea.ps1"; DestDir: "{app}"; Flags: ignoreversion Source: "uninstall_lea.ps1"; DestDir: "{app}"; Flags: ignoreversion
; Script de configuration du runtime Python embedded (optionnel) ; Script de configuration du runtime Python embedded (toujours installe)
Source: "configure_embed.ps1"; DestDir: "{app}"; Flags: ignoreversion; Components: pythonembed Source: "configure_embed.ps1"; DestDir: "{app}"; Flags: ignoreversion
; Licence CGU (affichee dans la page licence ET conservee dans {app}) ; Licence CGU (affichee dans la page licence ET conservee dans {app})
Source: "LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion isreadme Source: "LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion isreadme
@@ -115,37 +114,30 @@ Source: "LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion isreadme
Source: "config_template.txt"; DestDir: "{app}"; Flags: ignoreversion Source: "config_template.txt"; DestDir: "{app}"; Flags: ignoreversion
[Components] [Components]
Name: "core"; Description: "Lea (obligatoire)"; Types: full compact custom; Flags: fixed ; Composant unique fixe : pas de choix utilisateur (runtime embedded toujours inclus).
Name: "pythonembed"; Description: "Python 3.12 embedded (recommande si Python non installe sur le poste)"; Types: full ; Inno masque la page Composants quand il n'y a aucun composant selectionnable.
Name: "autostart"; Description: "Demarrer Lea automatiquement au demarrage de Windows"; Types: full Name: "core"; Description: "Lea"; Types: full compact custom; Flags: fixed
[Tasks] [Tasks]
Name: "autostart"; Description: "Demarrer Lea automatiquement au demarrage de Windows"; GroupDescription: "Options :"
Name: "desktopicon"; Description: "Creer un raccourci sur le bureau"; GroupDescription: "Raccourcis :"; Flags: unchecked Name: "desktopicon"; Description: "Creer un raccourci sur le bureau"; GroupDescription: "Raccourcis :"; Flags: unchecked
Name: "startmenuicon"; Description: "Creer un raccourci dans le menu Demarrer"; GroupDescription: "Raccourcis :" Name: "startmenuicon"; Description: "Creer un raccourci dans le menu Demarrer"; GroupDescription: "Raccourcis :"
[Icons] [Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"; Tasks: startmenuicon Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"; Tasks: startmenuicon
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"; Tasks: desktopicon Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"; Tasks: desktopicon
; Raccourci autostart (shell:startup) — cree si composant autostart selectionne ; Raccourci autostart (shell:startup) — cree si tache autostart selectionnee
Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; \ Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; \
WorkingDir: "{app}"; Components: autostart WorkingDir: "{app}"; Tasks: autostart
[Run] [Run]
; Apres copie : executer install.bat pour creer le venv et installer les dependances Python ; Configuration du runtime embedded : reecrit Lea.bat pour pointer sur python-embed.
; Skip si bundle embedded (dans ce cas, on utilise python-embed directement) ; TOUJOURS execute — runtime 100% autonome, aucune branche venv/pip/Python systeme.
Filename: "{app}\install.bat"; \
WorkingDir: "{app}"; \
StatusMsg: "Installation des composants Python (1-2 minutes)..."; \
Flags: runhidden waituntilterminated; \
Components: not pythonembed
; Configuration Python embedded : creer un Lea.bat qui pointe sur python-embed
Filename: "{cmd}"; \ Filename: "{cmd}"; \
Parameters: "/c copy /y ""{app}\Lea.bat"" ""{app}\Lea.bat.bak"" && powershell -NoProfile -ExecutionPolicy Bypass -File ""{app}\configure_embed.ps1"""; \ Parameters: "/c copy /y ""{app}\Lea.bat"" ""{app}\Lea.bat.bak"" && powershell -NoProfile -ExecutionPolicy Bypass -File ""{app}\configure_embed.ps1"""; \
WorkingDir: "{app}"; \ WorkingDir: "{app}"; \
StatusMsg: "Configuration du runtime Python embedded..."; \ StatusMsg: "Configuration de Lea..."; \
Flags: runhidden waituntilterminated; \ Flags: runhidden waituntilterminated
Components: pythonembed
; Lancer Lea a la fin de l'installation (optionnel) ; Lancer Lea a la fin de l'installation (optionnel)
Filename: "{app}\{#MyAppExeName}"; \ Filename: "{app}\{#MyAppExeName}"; \
@@ -161,13 +153,20 @@ Filename: "powershell.exe"; \
[UninstallDelete] [UninstallDelete]
Type: filesandordirs; Name: "{app}\.venv" Type: filesandordirs; Name: "{app}\.venv"
Type: filesandordirs; Name: "{app}\python-embed"
Type: filesandordirs; Name: "{app}\__pycache__" Type: filesandordirs; Name: "{app}\__pycache__"
Type: filesandordirs; Name: "{app}\agent_v1\__pycache__" Type: filesandordirs; Name: "{app}\agent_v1\__pycache__"
Type: filesandordirs; Name: "{app}\agent_v1\sessions" Type: filesandordirs; Name: "{app}\agent_v1\sessions"
Type: filesandordirs; Name: "{app}\agent_v1\logs" Type: filesandordirs; Name: "{app}\agent_v1\logs"
Type: files; Name: "{app}\lea_agent.lock" Type: files; Name: "{app}\lea_agent.lock"
Type: files; Name: "{app}\config.txt" Type: files; Name: "{app}\config.txt"
Type: files; Name: "{app}\config.txt.bak.*"
Type: files; Name: "{app}\machine_id.txt" Type: files; Name: "{app}\machine_id.txt"
Type: files; Name: "{app}\Lea.bat.bak"
Type: files; Name: "{app}\install.bat"
; Filet de securite : supprime tout residu genere au runtime (caches, *.pyc, logs)
; afin que le dossier applicatif soit entierement supprime (exigence desinstall propre).
Type: filesandordirs; Name: "{app}"
; ============================================================ ; ============================================================
; Code Pascal : pages custom + generation config.txt + helpers ; Code Pascal : pages custom + generation config.txt + helpers
@@ -176,7 +175,7 @@ Type: files; Name: "{app}\machine_id.txt"
const const
SERVER_URL_DEFAULT = 'https://lea.labs.laurinebazin.design/api/v1'; SERVER_URL_DEFAULT = 'https://lea.labs.laurinebazin.design/api/v1';
SERVER_HOST_DEFAULT = 'lea.labs.laurinebazin.design'; SERVER_HOST_DEFAULT = 'lea.labs.laurinebazin.design';
DEFAULT_TOKEN = '86031addb338e449fccdb1a983f61807aec15d42d482b9c7748ad607dc23caab'; DEFAULT_TOKEN = 'o3_LHqV_7_Gc6OVPHndhsBbvG6HJ5PCgl8yIBhGUIz8';
var var
EnrollmentPage: TInputQueryWizardPage; EnrollmentPage: TInputQueryWizardPage;

View File

@@ -106,6 +106,9 @@ rsync -a \
--exclude='test_lea_*' \ --exclude='test_lea_*' \
--exclude='_test_paused_toast.py' \ --exclude='_test_paused_toast.py' \
--exclude='tools/test_*' \ --exclude='tools/test_*' \
--exclude='install.bat' \
--exclude='*.bak' \
--exclude='config.txt.bak*' \
"$BASE_BUILD_DIR/" \ "$BASE_BUILD_DIR/" \
"$STAGING_DIR/" "$STAGING_DIR/"
@@ -131,15 +134,38 @@ echo ""
# 5. Python embedded (optionnel) # 5. Python embedded (optionnel)
# --------------------------------------------------------------- # ---------------------------------------------------------------
PYTHON_EMBED_SRC="${PYTHON_EMBED_DIR:-$SCRIPT_DIR/python-3.12-embed}" PYTHON_EMBED_SRC="${PYTHON_EMBED_DIR:-$SCRIPT_DIR/python-3.12-embed}"
if [[ -d "$PYTHON_EMBED_SRC" ]]; then if [[ ! -d "$PYTHON_EMBED_SRC" ]]; then
echo "[4/5] Copie de Python 3.12 embedded..." echo -e "${RED}[4/5] ERREUR : Python 3.12 embedded introuvable dans $PYTHON_EMBED_SRC${NC}"
rsync -a "$PYTHON_EMBED_SRC/" "$STAGING_DIR/python-3.12-embed/" echo " L'embed est OBLIGATOIRE (runtime 100% autonome, aucune dependance Python systeme)."
echo " Python embedded inclus" echo " Build interrompu."
else exit 1
echo -e "${YELLOW}[4/5] Python 3.12 embedded non trouve dans $PYTHON_EMBED_SRC${NC}"
echo " L'installeur sera produit SANS bundle Python."
echo " Pour bundler Python : voir README.md section 'Python embedded'"
fi fi
echo "[4/5] Copie de Python 3.12 embedded..."
rsync -a "$PYTHON_EMBED_SRC/" "$STAGING_DIR/python-3.12-embed/"
# Validation de la completude de l'embed : un embed incomplet = install cassee chez le client.
# La liste doit rester alignee avec configure_embed.ps1 (verification runtime des imports).
EMBED="$STAGING_DIR/python-3.12-embed"
REQUIRED_EMBED=(
"python.exe" "pythonw.exe" "python312._pth"
"_tkinter.pyd" "tcl86t.dll" "tk86t.dll" "zlib1.dll"
"Lib/site-packages/socketio" "Lib/site-packages/tkinter"
"Lib/site-packages/mss" "Lib/site-packages/pynput"
"Lib/site-packages/pystray" "Lib/site-packages/plyer"
"Lib/site-packages/requests" "Lib/site-packages/PIL"
"Lib/site-packages/win32"
)
MISSING_EMBED=()
for f in "${REQUIRED_EMBED[@]}"; do
[[ -e "$EMBED/$f" ]] || MISSING_EMBED+=("$f")
done
if [[ ${#MISSING_EMBED[@]} -gt 0 ]]; then
echo -e "${RED} ERREUR : embed incomplet. Elements manquants :${NC}"
printf ' - %s\n' "${MISSING_EMBED[@]}"
echo " Build interrompu (le runtime doit etre complet et autonome)."
exit 1
fi
echo " Python embedded complet inclus (${#REQUIRED_EMBED[@]} elements verifies)"
echo "" echo ""
# --------------------------------------------------------------- # ---------------------------------------------------------------

View File

@@ -40,34 +40,24 @@ if ($PthFile) {
} }
# --------------------------------------------------------------- # ---------------------------------------------------------------
# 2-3. Dependances Python # 2-3. Verification des dependances embarquees (runtime 100% autonome)
# Si l'embed est livre complet (deps + tkinter pre-embarques), # L'embed DOIT contenir toutes les dependances runtime.
# on saute le bootstrap pip / install : INSTALL HORS-LIGNE possible. # AUCUN pip, AUCUN reseau : si une dependance manque -> echec explicite.
# Sinon (embed nu), on bootstrap pip + installe requirements (mode online).
# --------------------------------------------------------------- # ---------------------------------------------------------------
$DepsOk = $false $RequiredModules = @('socketio','tkinter','mss','pynput','pystray','plyer','requests','PIL','win32api')
& $PythonExe -c "import socketio, tkinter" 2>$null $Missing = @()
if ($LASTEXITCODE -eq 0) { $DepsOk = $true } foreach ($m in $RequiredModules) {
& $PythonExe -c "import $m" 2>$null
if ($DepsOk) { if ($LASTEXITCODE -ne 0) { $Missing += $m }
Write-Host " Dependances deja embarquees (socketio + tkinter) - pip saute (offline OK)."
} else {
# Bootstrap pip (necessite internet)
$GetPip = Join-Path $env:TEMP "get-pip.py"
Write-Host " Telechargement de get-pip.py..."
Invoke-WebRequest -Uri "https://bootstrap.pypa.io/get-pip.py" -OutFile $GetPip -UseBasicParsing
Write-Host " Installation de pip..."
& $PythonExe $GetPip --no-warn-script-location
Remove-Item $GetPip -Force
$Requirements = Join-Path $AppDir "requirements_agent.txt"
if (Test-Path $Requirements) {
Write-Host " Installation des dependances Python..."
& $PythonExe -m pip install --no-warn-script-location -r $Requirements
}
} }
if ($Missing.Count -gt 0) {
Write-Host " ERREUR : runtime Lea incomplet. Modules manquants : $($Missing -join ', ')"
Write-Host " L'embed doit etre livre complet (aucune installation reseau en POC)."
exit 1
}
Write-Host " Dependances embarquees verifiees ($($RequiredModules.Count) modules) - offline OK."
# --------------------------------------------------------------- # ---------------------------------------------------------------
# 4. Reecrire Lea.bat pour utiliser python-embed # 4. Reecrire Lea.bat pour utiliser python-embed
# --------------------------------------------------------------- # ---------------------------------------------------------------

View File

@@ -7,6 +7,7 @@
import React from 'react'; import React from 'react';
import { CoachingSuggestion } from '../../hooks/useCoachingWebSocket'; import { CoachingSuggestion } from '../../hooks/useCoachingWebSocket';
import { getApiOrigin } from '../../services/apiClient';
interface CoachingSuggestionCardProps { interface CoachingSuggestionCardProps {
suggestion: CoachingSuggestion; suggestion: CoachingSuggestion;
@@ -106,7 +107,7 @@ const CoachingSuggestionCard: React.FC<CoachingSuggestionCardProps> = ({
{suggestion.screenshotPath && ( {suggestion.screenshotPath && (
<div className="suggestion-screenshot"> <div className="suggestion-screenshot">
<img <img
src={`http://localhost:5001${suggestion.screenshotPath}`} src={`${getApiOrigin()}${suggestion.screenshotPath}`}
alt="Target element" alt="Target element"
onError={(e) => { onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none'; (e.target as HTMLImageElement).style.display = 'none';

View File

@@ -23,6 +23,7 @@ import CoachingSuggestionCard from './CoachingSuggestionCard';
import CoachingDecisionButtons from './CoachingDecisionButtons'; import CoachingDecisionButtons from './CoachingDecisionButtons';
import CoachingStatsDisplay from './CoachingStatsDisplay'; import CoachingStatsDisplay from './CoachingStatsDisplay';
import CorrectionEditor from './CorrectionEditor'; import CorrectionEditor from './CorrectionEditor';
import { getApiOrigin } from '../../services/apiClient';
import './CoachingPanel.css'; import './CoachingPanel.css';
interface CoachingPanelProps { interface CoachingPanelProps {
@@ -143,7 +144,7 @@ export const CoachingPanel: React.FC<CoachingPanelProps> = ({
const startSession = useCallback( const startSession = useCallback(
async (wfId: string) => { async (wfId: string) => {
try { try {
const response = await fetch(`${serverUrl || 'http://localhost:5001'}/api/executions/coaching`, { const response = await fetch(`${serverUrl || getApiOrigin()}/api/executions/coaching`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ workflow_id: wfId }), body: JSON.stringify({ workflow_id: wfId }),

View File

@@ -62,6 +62,7 @@ import {
Variable, Variable,
} from '../../types'; } from '../../types';
import { VWBEvidence } from '../../types/evidence'; import { VWBEvidence } from '../../types/evidence';
import { getApiHost } from '../../services/apiClient';
interface VWBExecutorExtensionProps { interface VWBExecutorExtensionProps {
workflow: Workflow; workflow: Workflow;
@@ -229,11 +230,8 @@ const VWBExecutorExtension: React.FC<VWBExecutorExtensionProps> = ({
setFeedbackLoading(true); setFeedbackLoading(true);
try { try {
// Déterminer l'URL de l'API // URL de l'API résolue dynamiquement (compatible IP DGX / accès distant).
const hostname = window.location.hostname; const apiBase = getApiHost();
const apiBase = (hostname === 'localhost' || hostname === '127.0.0.1')
? 'http://localhost:5001/api'
: `http://${hostname}:5000/api`;
const response = await fetch(`${apiBase}/workflows/${workflow.id}/feedback`, { const response = await fetch(`${apiBase}/workflows/${workflow.id}/feedback`, {
method: 'POST', method: 'POST',

View File

@@ -62,6 +62,7 @@ import { captureLibraryService, SavedCapture } from '../../services/captureLibra
import { VisualSelection, BoundingBox } from '../../types'; import { VisualSelection, BoundingBox } from '../../types';
import { screenCaptureService } from '../../services/screenCaptureService'; import { screenCaptureService } from '../../services/screenCaptureService';
import { uploadAnchorImage } from '../../services/anchorImageService'; import { uploadAnchorImage } from '../../services/anchorImageService';
import { getApiHost } from '../../services/apiClient';
interface VisualSelectorProps { interface VisualSelectorProps {
isOpen: boolean; isOpen: boolean;
@@ -137,7 +138,7 @@ const VisualSelector: React.FC<VisualSelectorProps> = ({
useEffect(() => { useEffect(() => {
const loadMonitors = async () => { const loadMonitors = async () => {
try { 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(); const data = await response.json();
if (data.success && data.monitors) { if (data.success && data.monitors) {
setMonitors(data.monitors); setMonitors(data.monitors);
@@ -301,7 +302,7 @@ const VisualSelector: React.FC<VisualSelectorProps> = ({
await new Promise(resolve => setTimeout(resolve, delayMs)); await new Promise(resolve => setTimeout(resolve, delayMs));
// Utiliser l'API de capture réelle avec le moniteur sélectionné // 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', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({

View File

@@ -8,7 +8,7 @@
*/ */
import { useState, useCallback, useRef, useEffect, useMemo } from 'react'; 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'; import { WorkflowApiData } from '../types';
// Types pour les états de requête // Types pour les états de requête
@@ -215,7 +215,7 @@ export function useConnectionState() {
// Vérification DIRECTE au montage (SANS passer par apiClient singleton) // Vérification DIRECTE au montage (SANS passer par apiClient singleton)
const checkOnMount = async () => { const checkOnMount = async () => {
try { try {
const response = await fetch('http://localhost:5001/api/health', { const response = await fetch(`${getApiHost()}/health`, {
headers: { 'Accept': 'application/json' }, headers: { 'Accept': 'application/json' },
}); });

View File

@@ -10,6 +10,7 @@
import { useState, useEffect, useCallback, useRef } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
import { io, Socket } from 'socket.io-client'; import { io, Socket } from 'socket.io-client';
import { getApiOrigin } from '../services/apiClient';
// Types for COACHING mode // Types for COACHING mode
export type CoachingDecision = 'accept' | 'reject' | 'correct' | 'manual' | 'skip'; export type CoachingDecision = 'accept' | 'reject' | 'correct' | 'manual' | 'skip';
@@ -106,7 +107,7 @@ const convertStats = (backendStats: Record<string, any>): CoachingStats => {
export function useCoachingWebSocket( export function useCoachingWebSocket(
options: UseCoachingWebSocketOptions = {} options: UseCoachingWebSocketOptions = {}
): UseCoachingWebSocketReturn { ): UseCoachingWebSocketReturn {
const { serverUrl = 'http://localhost:5001', autoConnect = true } = options; const { serverUrl = getApiOrigin(), autoConnect = true } = options;
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const [isSubscribed, setIsSubscribed] = useState(false); const [isSubscribed, setIsSubscribed] = useState(false);

View File

@@ -11,7 +11,7 @@
*/ */
import { useState, useEffect, useCallback, useRef } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
import { apiClient, ConnectionState } from '../services/apiClient'; import { apiClient, ConnectionState, getApiHost } from '../services/apiClient';
interface ConnectionStatusState { interface ConnectionStatusState {
/** État actuel de la connexion */ /** État actuel de la connexion */
@@ -130,7 +130,7 @@ export function useConnectionStatus(options: UseConnectionStatusOptions = {}): C
// Vérification DIRECTE au démarrage // Vérification DIRECTE au démarrage
const checkOnMount = async () => { const checkOnMount = async () => {
try { try {
const response = await fetch('http://localhost:5001/api/health', { const response = await fetch(`${getApiHost()}/health`, {
headers: { 'Accept': 'application/json' }, headers: { 'Accept': 'application/json' },
}); });

View File

@@ -5,6 +5,7 @@
*/ */
import { useState, useCallback, useEffect } from 'react'; import { useState, useCallback, useEffect } from 'react';
import { getApiHost } from '../services/apiClient';
// Types // Types
export interface Correction { export interface Correction {
@@ -68,7 +69,8 @@ interface UseCorrectionPacksReturn {
selectPack: (pack: CorrectionPack | null) => void; 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 { export function useCorrectionPacks(): UseCorrectionPacksReturn {
const [packs, setPacks] = useState<CorrectionPack[]>([]); const [packs, setPacks] = useState<CorrectionPack[]>([]);

View File

@@ -9,6 +9,16 @@
import { BoundingBox } from '../types'; 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 { interface VisualMetadata {
element_type: string; element_type: string;
relative_position?: string; relative_position?: string;
@@ -63,7 +73,7 @@ class VisualCaptureService {
private cache: Map<string, any>; private cache: Map<string, any>;
private cacheTimeout: number; private cacheTimeout: number;
constructor(baseUrl: string = 'http://localhost:8000') { constructor(baseUrl: string = getCoreApiOrigin()) {
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
this.timeout = 30000; // 30 secondes this.timeout = 30000; // 30 secondes
this.cache = new Map(); this.cache = new Map();

View File

@@ -8,8 +8,28 @@
*/ */
import { BoundingBox } from '../types'; 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 { export interface AnchorImageUploadResult {
success: boolean; success: boolean;
@@ -67,7 +87,7 @@ export async function uploadAnchorImage(
anchorId anchorId
}); });
const response = await fetch(`${API_BASE}/api/anchor-images`, { const response = await fetch(`${apiBase()}/api/anchor-images`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -106,7 +126,7 @@ export async function uploadAnchorImage(
* @returns URL complète de la miniature * @returns URL complète de la miniature
*/ */
export function getThumbnailUrl(anchorId: string): string { 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 * @returns URL complète de l'image originale
*/ */
export function getOriginalUrl(anchorId: string): string { 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 * @returns Métadonnées de l'ancre
*/ */
export async function getAnchorMetadata(anchorId: string): Promise<AnchorMetadata> { 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) { if (!response.ok) {
throw new Error(`Ancre '${anchorId}' non trouvée`); 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 * @returns true si supprimé avec succès
*/ */
export async function deleteAnchorImage(anchorId: string): Promise<boolean> { 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', method: 'DELETE',
}); });
@@ -166,7 +186,7 @@ export async function listAnchorImages(
offset: number = 0 offset: number = 0
): Promise<{ anchors: AnchorMetadata[]; total: number }> { ): Promise<{ anchors: AnchorMetadata[]; total: number }> {
const response = await fetch( const response = await fetch(
`${API_BASE}/api/anchor-images?limit=${limit}&offset=${offset}` `${apiBase()}/api/anchor-images?limit=${limit}&offset=${offset}`
); );
if (!response.ok) { if (!response.ok) {
@@ -186,7 +206,7 @@ export async function listAnchorImages(
* @returns Statistiques de stockage * @returns Statistiques de stockage
*/ */
export async function getStorageStats(): Promise<StorageStats> { 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) { if (!response.ok) {
throw new Error('Erreur lors de la récupération des statistiques'); 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> { export async function anchorExists(anchorId: string): Promise<boolean> {
try { try {
const response = await fetch( const response = await fetch(
`${API_BASE}/api/anchor-images/${anchorId}/metadata`, `${apiBase()}/api/anchor-images/${anchorId}/metadata`,
{ method: 'HEAD' } { method: 'HEAD' }
); );
return response.ok; return response.ok;
@@ -229,17 +249,18 @@ export function getPreviewImageUrl(anchor: {
}): string | null { }): string | null {
// Priorité 1: URL de miniature serveur // Priorité 1: URL de miniature serveur
if (anchor.thumbnail_url) { 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') return anchor.thumbnail_url.startsWith('http')
? anchor.thumbnail_url ? normalizeAnchorUrl(anchor.thumbnail_url)
: `${API_BASE}${anchor.thumbnail_url}`; : `${apiBase()}${anchor.thumbnail_url}`;
} }
// Priorité 2: URL d'image originale serveur // Priorité 2: URL d'image originale serveur
if (anchor.reference_image_url) { if (anchor.reference_image_url) {
return anchor.reference_image_url.startsWith('http') return anchor.reference_image_url.startsWith('http')
? anchor.reference_image_url ? normalizeAnchorUrl(anchor.reference_image_url)
: `${API_BASE}${anchor.reference_image_url}`; : `${apiBase()}${anchor.reference_image_url}`;
} }
// Priorité 3: Construire l'URL depuis anchor_id si présent // Priorité 3: Construire l'URL depuis anchor_id si présent

View File

@@ -46,20 +46,33 @@ type ConnectionState = 'online' | 'offline' | 'checking';
// Callbacks pour les changements d'état // Callbacks pour les changements d'état
type ConnectionStateCallback = (state: ConnectionState) => void; type ConnectionStateCallback = (state: ConnectionState) => void;
// Détection automatique de l'hôte pour support multi-machines // 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 // Si on accède via une IP ou un nom DNS DGX, utiliser cet hôte pour l'API.
// Sinon utiliser localhost //
const getApiHost = (): string => { // 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') { if (typeof window !== 'undefined') {
const hostname = window.location.hostname; const hostname = window.location.hostname;
// Si c'est localhost ou 127.0.0.1, garder localhost // En accès distant comme en local, le backend Flask reste sur le port 5001
if (hostname === 'localhost' || hostname === '127.0.0.1') { return `http://${hostname}:5001`;
return 'http://localhost:5001/api';
} }
// Sinon utiliser le même hostname (IP) avec le port 5000 return (process.env.REACT_APP_API_ORIGIN || '').replace(/\/$/, '');
return `http://${hostname}:5000/api`; };
}
return 'http://localhost:5001/api'; /**
* 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 // Configuration par défaut

View File

@@ -46,6 +46,9 @@ import {
getStaticCatalogStats, getStaticCatalogStats,
} from '../data/staticCatalog'; } from '../data/staticCatalog';
// Origine de l'API résolue dynamiquement (compatible IP DGX / accès distant).
import { getApiOrigin } from './apiClient';
// Configuration du service catalogue // Configuration du service catalogue
interface CatalogServiceConfig { interface CatalogServiceConfig {
urls: string[]; urls: string[];
@@ -173,18 +176,17 @@ class CatalogService {
const currentOrigin = window.location.origin; const currentOrigin = window.location.origin;
candidateUrls.push(currentOrigin); candidateUrls.push(currentOrigin);
// 4. Localhost standard (développement) - Port 5001 en priorité // 4. Origine API courante (port 5001), résolue dynamiquement.
if (!candidateUrls.includes('http://localhost:5001')) { // En accès navigateur -> http://<hostname>:5001
candidateUrls.push('http://localhost:5001'); const apiOrigin = getApiOrigin();
} if (!candidateUrls.includes(apiOrigin)) {
if (!candidateUrls.includes('http://localhost:5001')) { candidateUrls.push(apiOrigin);
candidateUrls.push('http://localhost:5001');
} }
// 5. IP locale détectée (cross-machine) // 5. IP locale détectée (cross-machine)
try { try {
const localIp = this.detectLocalIp(); 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}:5000`);
candidateUrls.push(`http://${localIp}:5004`); candidateUrls.push(`http://${localIp}:5004`);
} }

View File

@@ -4,13 +4,15 @@
*/ */
import { VWBEvidence, EvidenceFilters, EvidenceExportOptions, EvidenceStats, EvidenceUtils } from '../types/evidence'; import { VWBEvidence, EvidenceFilters, EvidenceExportOptions, EvidenceStats, EvidenceUtils } from '../types/evidence';
import { getApiOrigin } from './apiClient';
export class EvidenceService { export class EvidenceService {
private baseUrl: string; private baseUrl: string;
private cache: Map<string, VWBEvidence[]> = new Map(); private cache: Map<string, VWBEvidence[]> = new Map();
private cacheTimeout: number = 5 * 60 * 1000; // 5 minutes 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; this.baseUrl = baseUrl;
} }

View File

@@ -6,8 +6,11 @@
* en utilisant le service RealScreenCaptureService du backend. * en utilisant le service RealScreenCaptureService du backend.
*/ */
import { getApiHost } from './apiClient';
// Configuration du service // 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 const REQUEST_TIMEOUT = 20000; // 20 secondes pour la capture avec détection
// Types pour les réponses API // Types pour les réponses API

View File

@@ -7,9 +7,11 @@
*/ */
import { BoundingBox, VisualSelection } from '../types'; import { BoundingBox, VisualSelection } from '../types';
import { getApiHost } from './apiClient';
// Configuration du service // 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 const REQUEST_TIMEOUT = 15000; // 15 secondes pour la capture d'écran
// Types pour les réponses API // Types pour les réponses API