feat: journée 17 avril — tests E2E validés, dashboard fleet+audit, VWB bridge, cleaner C2
Some checks failed
security-audit / Bandit (scan statique) (push) Successful in 14s
security-audit / pip-audit (CVE dépendances) (push) Successful in 10s
security-audit / Scan secrets (grep) (push) Successful in 8s
tests / Lint (ruff + black) (push) Successful in 13s
tests / Tests unitaires (sans GPU) (push) Failing after 14s
tests / Tests sécurité (critique) (push) Has been skipped

Pipeline E2E complet validé :
  Capture VM → streaming → serveur → cleaner → replay → audit trail
  Mode apprentissage supervisé fonctionne (Léa échoue → humain → reprise)

Dashboard :
  - Cleanup 14→10 onglets (RCE supprimée)
  - Fleet : enregistrer/révoquer agents, tokens, ZIP pré-configuré téléchargeable
  - Audit trail MVP (/audit) : filtres, tableau, export CSV, conformité AI Act/RGPD
  - Formulaire Fleet simplifié (nom + email, machine_id auto)

VWB bridge Léa→VWB :
  - Compound décomposés en N steps (saisie + raccourci visibles)
  - Layout serpentin 3 colonnes (plus colonne verticale)
  - Badge OS 🪟/🐧, filtre OS retiré (admin Linux voit Windows)
  - Fix import SQLite readonly

Cleaner intelligent :
  - Descriptions lisibles (UIA/C2) + détection doublons
  - Logique C2 : UIElement identifié = jamais parasite
  - Patterns parasites resserrés
  - Message Léa : "Je n'y arrive pas, montrez-moi comment faire"

Config agent (INC-1 à INC-7) :
  - SERVER_URL + SERVER_BASE unifiés
  - RPA_OLLAMA_HOST séparé
  - allow_redirects=False sur POST
  - Middleware réécriture URL serveur

CI Gitea : fix token + Flask-SocketIO + ruff propre
Fleet endpoints : /agents/enroll|uninstall|fleet + agent_registry SQLite
Backup : script quotidien workflows.db + audit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-04-17 17:46:40 +02:00
parent 2fa864b5c7
commit 4f61741420
27 changed files with 5088 additions and 1543 deletions

View File

@@ -21,36 +21,33 @@ from typing import Any, Callable, Dict, List, Optional
logger = logging.getLogger("lea_ui.server_client")
def _get_server_host() -> str:
"""Recuperer l'adresse du serveur Linux.
def _get_server_url() -> str:
"""Recuperer l'URL du serveur RPA (avec /api/v1).
Ordre de resolution :
1. Variable d'environnement RPA_SERVER_HOST
2. Fichier de config agent_config.json (cle "server_host")
3. Fallback localhost
1. Import depuis agent_v1.config (source de verite unique)
2. Variable d'environnement RPA_SERVER_URL
3. Fallback http://localhost:5005/api/v1
"""
# 1. Variable d'environnement
host = os.environ.get("RPA_SERVER_HOST", "").strip()
if host:
return host
# 1. Import depuis config.py (source de verite)
try:
from agent_v1.config import SERVER_URL
return SERVER_URL
except ImportError:
pass
# 2. Fichier de config
config_paths = [
os.path.join(os.path.dirname(__file__), "..", "agent_config.json"),
os.path.join(os.path.dirname(__file__), "..", "..", "agent_config.json"),
]
for config_path in config_paths:
try:
with open(config_path, "r", encoding="utf-8") as f:
cfg = json.load(f)
host = cfg.get("server_host", "").strip()
if host:
return host
except (OSError, json.JSONDecodeError):
continue
# 2. Variable d'environnement directe
url = os.environ.get("RPA_SERVER_URL", "").strip().rstrip("/")
if url:
return url
# 3. Fallback
return "localhost"
return "http://localhost:5005/api/v1"
def _get_server_base(server_url: str) -> str:
"""Extraire la base URL (sans /api/v1) pour les routes racine (/health)."""
return server_url.rsplit("/api/v1", 1)[0]
class LeaServerClient:
@@ -67,19 +64,22 @@ class LeaServerClient:
chat_port: int = 5004,
stream_port: int = 5005,
) -> None:
self._host = server_host or _get_server_host()
# URL unifiée : SERVER_URL contient TOUJOURS /api/v1 (convention INC-1).
# _stream_url = URL avec /api/v1 (pour les routes API)
# _stream_base = URL sans /api/v1 (pour /health uniquement)
self._stream_url = _get_server_url()
self._stream_base = _get_server_base(self._stream_url)
# Extraire le host depuis l'URL pour le chat et pour l'affichage
try:
from urllib.parse import urlparse
parsed = urlparse(self._stream_base)
self._host = parsed.hostname or "localhost"
except Exception:
self._host = server_host or "localhost"
self._chat_port = chat_port
self._stream_port = stream_port
# En prod, la base URL passe par le reverse proxy HTTPS
# (ex. https://lea.labs.laurinebazin.design). Si RPA_SERVER_URL est
# definie on l'utilise telle quelle, sinon on reconstruit http://host:port.
server_url = os.environ.get("RPA_SERVER_URL", "").strip().rstrip("/")
if server_url:
self._stream_base = server_url
else:
self._stream_base = f"http://{self._host}:{self._stream_port}"
self._chat_base = f"http://{self._host}:{self._chat_port}"
# Etat de connexion
@@ -103,8 +103,8 @@ class LeaServerClient:
self._api_token = os.environ.get("RPA_API_TOKEN", "")
logger.info(
"LeaServerClient initialise : chat=%s, stream=%s",
self._chat_base, self._stream_base,
"LeaServerClient initialise : chat=%s, stream_url=%s, stream_base=%s",
self._chat_base, self._stream_url, self._stream_base,
)
# ---------------------------------------------------------------------------
@@ -154,7 +154,11 @@ class LeaServerClient:
# ---------------------------------------------------------------------------
def check_connection(self) -> bool:
"""Tester la connexion au serveur streaming (port 5005)."""
"""Tester la connexion au serveur streaming (port 5005).
Le health check utilise _stream_base (sans /api/v1) car la route
/health est a la racine du serveur FastAPI, pas sous /api/v1.
"""
try:
import requests
resp = requests.get(
@@ -227,7 +231,7 @@ class LeaServerClient:
import requests
headers = self._auth_headers()
resp = requests.get(
f"{self._stream_base}/api/v1/traces/stream/workflows",
f"{self._stream_url}/traces/stream/workflows",
headers=headers,
timeout=10,
)
@@ -284,7 +288,7 @@ class LeaServerClient:
while self._polling:
try:
resp = req_lib.get(
f"{self._stream_base}/api/v1/traces/stream/replay/next",
f"{self._stream_url}/traces/stream/replay/next",
params={"session_id": self._poll_session_id},
headers=self._auth_headers(),
timeout=5,
@@ -318,7 +322,7 @@ class LeaServerClient:
try:
import requests
resp = requests.get(
f"{self._stream_base}/api/v1/traces/stream/replays",
f"{self._stream_url}/traces/stream/replays",
headers=self._auth_headers(),
timeout=5,
)
@@ -346,7 +350,7 @@ class LeaServerClient:
try:
import requests
requests.post(
f"{self._stream_base}/api/v1/traces/stream/replay/result",
f"{self._stream_url}/traces/stream/replay/result",
json={
"session_id": session_id,
"action_id": action_id,