feat(dashboard): session cleaner intégré + auth + nettoyage UI
- Onglet "🧹 Nettoyage" dans le dashboard (iframe vers port 5006) - Indicateur d'état + bouton de démarrage si cleaner down - Service systemd rpa-session-cleaner intégré au target rpa-vision - svc.sh et services.conf incluent session-cleaner (port 5006) P0-A — Auth dashboard Flask : - HTTP Basic obligatoire sur tous les endpoints (sauf /health, /healthz) - Credentials via DASHBOARD_USER + DASHBOARD_PASSWORD - 13 tests Nettoyage UI : - Section "Détection Visuelle" OWL retirée (modèle remplacé par pipeline VLM) - Dashboard préfère auto shot_*_blurred.png (avec ?raw=1 pour brut) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
42
deploy/systemd/rpa-session-cleaner.service
Normal file
42
deploy/systemd/rpa-session-cleaner.service
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=RPA Vision V3 - Session Cleaner (port 5006)
|
||||||
|
Documentation=https://lea.labs.laurinebazin.design
|
||||||
|
After=network-online.target rpa-streaming.service
|
||||||
|
Wants=network-online.target
|
||||||
|
Requires=rpa-streaming.service
|
||||||
|
PartOf=rpa-vision.target
|
||||||
|
StartLimitIntervalSec=300
|
||||||
|
StartLimitBurst=5
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
|
||||||
|
# ---- Runtime ----
|
||||||
|
User=dom
|
||||||
|
Group=dom
|
||||||
|
WorkingDirectory=/home/dom/ai/rpa_vision_v3
|
||||||
|
EnvironmentFile=/home/dom/ai/rpa_vision_v3/.env.local
|
||||||
|
Environment="PYTHONUNBUFFERED=1"
|
||||||
|
Environment="RPA_SERVICE_NAME=rpa-session-cleaner"
|
||||||
|
|
||||||
|
# Lancement du session cleaner (dépend du streaming server port 5005)
|
||||||
|
ExecStart=/home/dom/ai/rpa_vision_v3/.venv/bin/python3 tools/session_cleaner.py
|
||||||
|
|
||||||
|
# ---- Resilience ----
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
TimeoutStopSec=15
|
||||||
|
KillMode=mixed
|
||||||
|
KillSignal=SIGTERM
|
||||||
|
|
||||||
|
# ---- Hardening ----
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
|
||||||
|
# Logs -> journald
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=rpa-session-cleaner
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=rpa-vision.target
|
||||||
7
deploy/systemd/rpa-vision.target
Normal file
7
deploy/systemd/rpa-vision.target
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=RPA Vision V3 - Tous les services
|
||||||
|
After=network-online.target
|
||||||
|
Wants=rpa-streaming.service rpa-vision-v3-api.service rpa-vision-v3-dashboard.service rpa-vision-v3-worker.service rpa-session-cleaner.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
# 5003 - Monitoring (métriques système)
|
# 5003 - Monitoring (métriques système)
|
||||||
# 5004 - Agent Chat (interface conversationnelle)
|
# 5004 - Agent Chat (interface conversationnelle)
|
||||||
# 5005 - Streaming Server (Agent V1 → core pipeline)
|
# 5005 - Streaming Server (Agent V1 → core pipeline)
|
||||||
|
# 5006 - Session Cleaner (nettoyage sessions avant replay)
|
||||||
# 3002 - VWB Frontend (Vite/React)
|
# 3002 - VWB Frontend (Vite/React)
|
||||||
#
|
#
|
||||||
|
|
||||||
@@ -20,3 +21,4 @@ agent-chat|5004|agent_chat/app.py|optional
|
|||||||
streaming|5005|agent_v0/server_v1/api_stream.py|optional
|
streaming|5005|agent_v0/server_v1/api_stream.py|optional
|
||||||
worker|5099|agent_v0/server_v1/run_worker.py|optional
|
worker|5099|agent_v0/server_v1/run_worker.py|optional
|
||||||
vwb-frontend|3002|cd visual_workflow_builder/frontend_v4 && npm run dev|required
|
vwb-frontend|3002|cd visual_workflow_builder/frontend_v4 && npm run dev|required
|
||||||
|
session-cleaner|5006|tools/session_cleaner.py|optional
|
||||||
|
|||||||
24
svc.sh
24
svc.sh
@@ -56,6 +56,7 @@ declare -A PORTS=(
|
|||||||
[streaming]=5005
|
[streaming]=5005
|
||||||
[worker]=5099
|
[worker]=5099
|
||||||
[vwb-frontend]=3002
|
[vwb-frontend]=3002
|
||||||
|
[session-cleaner]=5006
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mapping nom court -> nom service systemd
|
# Mapping nom court -> nom service systemd
|
||||||
@@ -66,13 +67,14 @@ declare -A SYSTEMD_UNITS=(
|
|||||||
[streaming]="rpa-streaming.service"
|
[streaming]="rpa-streaming.service"
|
||||||
[worker]="rpa-worker.service"
|
[worker]="rpa-worker.service"
|
||||||
[vwb-frontend]="rpa-vwb-frontend.service"
|
[vwb-frontend]="rpa-vwb-frontend.service"
|
||||||
|
[session-cleaner]="rpa-session-cleaner.service"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Services gérés par systemd (ceux qui ont un .service)
|
# Services gérés par systemd (ceux qui ont un .service)
|
||||||
SYSTEMD_SERVICES="streaming worker agent-chat dashboard vwb-backend vwb-frontend"
|
SYSTEMD_SERVICES="streaming worker agent-chat dashboard vwb-backend vwb-frontend session-cleaner"
|
||||||
|
|
||||||
# Tous les services connus
|
# Tous les services connus
|
||||||
ALL_SERVICES="api dashboard vwb-backend monitoring agent-chat streaming worker vwb-frontend"
|
ALL_SERVICES="api dashboard vwb-backend monitoring agent-chat streaming worker vwb-frontend session-cleaner"
|
||||||
|
|
||||||
declare -A COMMANDS=(
|
declare -A COMMANDS=(
|
||||||
[api]="$VENV_DIR/bin/python3 server/api_upload.py"
|
[api]="$VENV_DIR/bin/python3 server/api_upload.py"
|
||||||
@@ -83,14 +85,15 @@ declare -A COMMANDS=(
|
|||||||
[streaming]="$VENV_DIR/bin/python3 -m agent_v0.server_v1.api_stream"
|
[streaming]="$VENV_DIR/bin/python3 -m agent_v0.server_v1.api_stream"
|
||||||
[worker]="$VENV_DIR/bin/python3 -m agent_v0.server_v1.run_worker"
|
[worker]="$VENV_DIR/bin/python3 -m agent_v0.server_v1.run_worker"
|
||||||
[vwb-frontend]="cd $SCRIPT_DIR/visual_workflow_builder/frontend_v4 && npm run dev"
|
[vwb-frontend]="cd $SCRIPT_DIR/visual_workflow_builder/frontend_v4 && npm run dev"
|
||||||
|
[session-cleaner]="$VENV_DIR/bin/python3 tools/session_cleaner.py"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Groupes de services
|
# Groupes de services
|
||||||
declare -A SVC_GROUPS=(
|
declare -A SVC_GROUPS=(
|
||||||
[vwb]="vwb-backend vwb-frontend"
|
[vwb]="vwb-backend vwb-frontend"
|
||||||
[all]="api dashboard vwb-backend vwb-frontend"
|
[all]="api dashboard vwb-backend vwb-frontend"
|
||||||
[full]="api dashboard vwb-backend vwb-frontend monitoring agent-chat streaming worker"
|
[full]="api dashboard vwb-backend vwb-frontend monitoring agent-chat streaming worker session-cleaner"
|
||||||
[boot]="streaming worker agent-chat dashboard vwb-backend vwb-frontend"
|
[boot]="streaming worker agent-chat dashboard vwb-backend vwb-frontend session-cleaner"
|
||||||
)
|
)
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -353,7 +356,7 @@ do_install() {
|
|||||||
|
|
||||||
# Vérifier que les fichiers existent
|
# Vérifier que les fichiers existent
|
||||||
local missing=false
|
local missing=false
|
||||||
for unit in rpa-streaming.service rpa-worker.service rpa-agent-chat.service rpa-dashboard.service rpa-vwb-backend.service rpa-vwb-frontend.service rpa-vision.target; do
|
for unit in rpa-streaming.service rpa-worker.service rpa-agent-chat.service rpa-dashboard.service rpa-vwb-backend.service rpa-vwb-frontend.service rpa-session-cleaner.service rpa-vision.target; do
|
||||||
if [ -f "$SYSTEMD_DIR/$unit" ]; then
|
if [ -f "$SYSTEMD_DIR/$unit" ]; then
|
||||||
echo -e " ${GREEN}OK${NC} $unit"
|
echo -e " ${GREEN}OK${NC} $unit"
|
||||||
else
|
else
|
||||||
@@ -397,7 +400,7 @@ do_enable() {
|
|||||||
echo -e "${CYAN}${BOLD}Activation du demarrage automatique au boot...${NC}"
|
echo -e "${CYAN}${BOLD}Activation du demarrage automatique au boot...${NC}"
|
||||||
systemctl --user daemon-reload
|
systemctl --user daemon-reload
|
||||||
systemctl --user enable rpa-vision.target
|
systemctl --user enable rpa-vision.target
|
||||||
for unit in rpa-streaming.service rpa-worker.service rpa-agent-chat.service rpa-dashboard.service rpa-vwb-backend.service rpa-vwb-frontend.service; do
|
for unit in rpa-streaming.service rpa-worker.service rpa-agent-chat.service rpa-dashboard.service rpa-vwb-backend.service rpa-vwb-frontend.service rpa-session-cleaner.service; do
|
||||||
systemctl --user enable "$unit" 2>/dev/null
|
systemctl --user enable "$unit" 2>/dev/null
|
||||||
echo -e " ${GREEN}OK${NC} $unit"
|
echo -e " ${GREEN}OK${NC} $unit"
|
||||||
done
|
done
|
||||||
@@ -408,7 +411,7 @@ do_enable() {
|
|||||||
do_disable() {
|
do_disable() {
|
||||||
echo -e "${YELLOW}${BOLD}Desactivation du demarrage automatique...${NC}"
|
echo -e "${YELLOW}${BOLD}Desactivation du demarrage automatique...${NC}"
|
||||||
systemctl --user disable rpa-vision.target 2>/dev/null || true
|
systemctl --user disable rpa-vision.target 2>/dev/null || true
|
||||||
for unit in rpa-streaming.service rpa-worker.service rpa-agent-chat.service rpa-dashboard.service rpa-vwb-backend.service rpa-vwb-frontend.service; do
|
for unit in rpa-streaming.service rpa-worker.service rpa-agent-chat.service rpa-dashboard.service rpa-vwb-backend.service rpa-vwb-frontend.service rpa-session-cleaner.service; do
|
||||||
systemctl --user disable "$unit" 2>/dev/null || true
|
systemctl --user disable "$unit" 2>/dev/null || true
|
||||||
echo -e " ${GREEN}OK${NC} $unit"
|
echo -e " ${GREEN}OK${NC} $unit"
|
||||||
done
|
done
|
||||||
@@ -438,11 +441,12 @@ show_help() {
|
|||||||
echo " dashboard Web Dashboard (port 5001)"
|
echo " dashboard Web Dashboard (port 5001)"
|
||||||
echo " vwb-backend VWB Backend Flask (port 5002)"
|
echo " vwb-backend VWB Backend Flask (port 5002)"
|
||||||
echo " vwb-frontend VWB Frontend Vite (port 3002)"
|
echo " vwb-frontend VWB Frontend Vite (port 3002)"
|
||||||
|
echo " session-cleaner Session Cleaner (port 5006)"
|
||||||
echo " api API Server (port 8000) [legacy uniquement]"
|
echo " api API Server (port 8000) [legacy uniquement]"
|
||||||
echo " monitoring Monitoring (port 5003) [legacy uniquement]"
|
echo " monitoring Monitoring (port 5003) [legacy uniquement]"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BOLD}Groupes:${NC}"
|
echo -e "${BOLD}Groupes:${NC}"
|
||||||
echo " boot Services systemd (streaming, worker, chat, dashboard, vwb)"
|
echo " boot Services systemd (streaming, worker, chat, dashboard, vwb, session-cleaner)"
|
||||||
echo " vwb VWB backend + frontend"
|
echo " vwb VWB backend + frontend"
|
||||||
echo " all Core (api, dashboard, vwb)"
|
echo " all Core (api, dashboard, vwb)"
|
||||||
echo " full Tous les services"
|
echo " full Tous les services"
|
||||||
@@ -451,8 +455,8 @@ show_help() {
|
|||||||
echo " --legacy Forcer le mode legacy (PID files au lieu de systemd)"
|
echo " --legacy Forcer le mode legacy (PID files au lieu de systemd)"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BOLD}Exemples:${NC}"
|
echo -e "${BOLD}Exemples:${NC}"
|
||||||
echo " $0 start boot # Demarrer les 5 services systemd"
|
echo " $0 start boot # Demarrer les services systemd"
|
||||||
echo " $0 stop boot # Arreter les 5 services systemd"
|
echo " $0 stop boot # Arreter les services systemd"
|
||||||
echo " $0 restart streaming # Redemarrer le streaming server"
|
echo " $0 restart streaming # Redemarrer le streaming server"
|
||||||
echo " $0 logs streaming -f # Suivre les logs du streaming"
|
echo " $0 logs streaming -f # Suivre les logs du streaming"
|
||||||
echo " $0 status # Voir l'etat de tout"
|
echo " $0 status # Voir l'etat de tout"
|
||||||
|
|||||||
160
tests/unit/test_dashboard_auth_p0a.py
Normal file
160
tests/unit/test_dashboard_auth_p0a.py
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
"""
|
||||||
|
Tests du Fix P0-A : authentification HTTP Basic sur le dashboard Flask (port 5001).
|
||||||
|
|
||||||
|
Avant ce fix, 71 endpoints étaient exposés sans auth. Le middleware
|
||||||
|
`_dashboard_basic_auth_middleware` ajoute un challenge 401 sur toutes les
|
||||||
|
routes HTTP sauf les healthchecks publics.
|
||||||
|
|
||||||
|
Contrôles :
|
||||||
|
- Sans Authorization header → 401 avec WWW-Authenticate
|
||||||
|
- Avec mauvais credentials → 401
|
||||||
|
- Avec bons credentials → passage normal (200)
|
||||||
|
- /health, /healthz, /api/health restent publics (monitoring externe)
|
||||||
|
- Mode TESTING sans DASHBOARD_AUTH_ENABLED → bypass (compat tests existants)
|
||||||
|
- DASHBOARD_AUTH_DISABLED=true → bypass global (dev local)
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import importlib
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# Ajouter le répertoire racine au path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def auth_enabled_client(monkeypatch):
|
||||||
|
"""Client Flask avec l'auth activée (TESTING + DASHBOARD_AUTH_ENABLED).
|
||||||
|
|
||||||
|
On recharge le module pour forcer la relecture des variables d'env.
|
||||||
|
"""
|
||||||
|
monkeypatch.setenv("DASHBOARD_USER", "lea")
|
||||||
|
monkeypatch.setenv("DASHBOARD_PASSWORD", "secret-test-pwd")
|
||||||
|
monkeypatch.delenv("DASHBOARD_AUTH_DISABLED", raising=False)
|
||||||
|
|
||||||
|
# Recharger le module pour que les constantes soient relues
|
||||||
|
if "web_dashboard.app" in sys.modules:
|
||||||
|
importlib.reload(sys.modules["web_dashboard.app"])
|
||||||
|
from web_dashboard.app import app
|
||||||
|
|
||||||
|
app.config["TESTING"] = True
|
||||||
|
app.config["DASHBOARD_AUTH_ENABLED"] = True
|
||||||
|
with app.test_client() as c:
|
||||||
|
yield c
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def auth_disabled_client(monkeypatch):
|
||||||
|
"""Client Flask avec l'auth désactivée (bypass global)."""
|
||||||
|
monkeypatch.setenv("DASHBOARD_AUTH_DISABLED", "true")
|
||||||
|
if "web_dashboard.app" in sys.modules:
|
||||||
|
importlib.reload(sys.modules["web_dashboard.app"])
|
||||||
|
from web_dashboard.app import app
|
||||||
|
|
||||||
|
app.config["TESTING"] = True
|
||||||
|
with app.test_client() as c:
|
||||||
|
yield c
|
||||||
|
|
||||||
|
|
||||||
|
def _basic_auth_header(user: str, password: str) -> str:
|
||||||
|
token = base64.b64encode(f"{user}:{password}".encode()).decode()
|
||||||
|
return f"Basic {token}"
|
||||||
|
|
||||||
|
|
||||||
|
class TestDashboardAuthP0A:
|
||||||
|
"""Fix P0-A : auth HTTP Basic obligatoire sur le dashboard."""
|
||||||
|
|
||||||
|
def test_no_auth_header_returns_401(self, auth_enabled_client):
|
||||||
|
"""Sans header Authorization → 401 + challenge WWW-Authenticate."""
|
||||||
|
resp = auth_enabled_client.get("/api/system/status")
|
||||||
|
assert resp.status_code == 401
|
||||||
|
assert "WWW-Authenticate" in resp.headers
|
||||||
|
assert "Basic" in resp.headers["WWW-Authenticate"]
|
||||||
|
|
||||||
|
def test_wrong_password_returns_401(self, auth_enabled_client):
|
||||||
|
"""Mauvais mot de passe → 401."""
|
||||||
|
resp = auth_enabled_client.get(
|
||||||
|
"/api/system/status",
|
||||||
|
headers={"Authorization": _basic_auth_header("lea", "wrong")},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 401
|
||||||
|
|
||||||
|
def test_wrong_user_returns_401(self, auth_enabled_client):
|
||||||
|
"""Mauvais utilisateur → 401."""
|
||||||
|
resp = auth_enabled_client.get(
|
||||||
|
"/api/system/status",
|
||||||
|
headers={"Authorization": _basic_auth_header("intruder", "secret-test-pwd")},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 401
|
||||||
|
|
||||||
|
def test_malformed_header_returns_401(self, auth_enabled_client):
|
||||||
|
"""Header mal formé (pas de Basic) → 401."""
|
||||||
|
resp = auth_enabled_client.get(
|
||||||
|
"/api/system/status",
|
||||||
|
headers={"Authorization": "Bearer tototoken"},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 401
|
||||||
|
|
||||||
|
def test_valid_credentials_pass(self, auth_enabled_client):
|
||||||
|
"""Bons credentials → 200."""
|
||||||
|
resp = auth_enabled_client.get(
|
||||||
|
"/api/system/status",
|
||||||
|
headers={"Authorization": _basic_auth_header("lea", "secret-test-pwd")},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
def test_healthz_public(self, auth_enabled_client):
|
||||||
|
"""/healthz reste public (systemd healthcheck)."""
|
||||||
|
resp = auth_enabled_client.get("/healthz")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
def test_health_public(self, auth_enabled_client):
|
||||||
|
"""/health reste public (monitoring externe)."""
|
||||||
|
resp = auth_enabled_client.get("/health")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
def test_api_health_public(self, auth_enabled_client):
|
||||||
|
"""/api/health reste public (NPM reverse proxy)."""
|
||||||
|
resp = auth_enabled_client.get("/api/health")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
def test_auth_disabled_bypass(self, auth_disabled_client):
|
||||||
|
"""DASHBOARD_AUTH_DISABLED=true → pas d'auth requise."""
|
||||||
|
resp = auth_disabled_client.get("/api/system/status")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
def test_config_endpoint_requires_auth(self, auth_enabled_client):
|
||||||
|
"""L'endpoint sensible /api/config exige l'auth."""
|
||||||
|
resp = auth_enabled_client.get("/api/config")
|
||||||
|
assert resp.status_code == 401
|
||||||
|
|
||||||
|
def test_services_endpoint_requires_auth(self, auth_enabled_client):
|
||||||
|
"""L'endpoint sensible /api/services exige l'auth."""
|
||||||
|
resp = auth_enabled_client.get("/api/services")
|
||||||
|
assert resp.status_code == 401
|
||||||
|
|
||||||
|
def test_services_start_all_requires_auth(self, auth_enabled_client):
|
||||||
|
"""Un endpoint POST destructeur exige l'auth."""
|
||||||
|
resp = auth_enabled_client.post("/api/services/start-all")
|
||||||
|
assert resp.status_code == 401
|
||||||
|
|
||||||
|
def test_index_page_requires_auth(self, auth_enabled_client):
|
||||||
|
"""Même la page HTML d'accueil exige l'auth (pas de leak côté public)."""
|
||||||
|
resp = auth_enabled_client.get("/")
|
||||||
|
assert resp.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _restore_module(monkeypatch):
|
||||||
|
"""Recharge web_dashboard.app après chaque test pour que les autres
|
||||||
|
tests (TestDashboardRoutes sans auth explicite) continuent de passer."""
|
||||||
|
yield
|
||||||
|
monkeypatch.delenv("DASHBOARD_AUTH_DISABLED", raising=False)
|
||||||
|
monkeypatch.delenv("DASHBOARD_USER", raising=False)
|
||||||
|
monkeypatch.delenv("DASHBOARD_PASSWORD", raising=False)
|
||||||
|
if "web_dashboard.app" in sys.modules:
|
||||||
|
importlib.reload(sys.modules["web_dashboard.app"])
|
||||||
@@ -7,8 +7,16 @@ Fonctionnalités:
|
|||||||
- Gestion des workflows
|
- Gestion des workflows
|
||||||
- Visualisation des sessions et screenshots
|
- Visualisation des sessions et screenshots
|
||||||
- Graphiques de performance
|
- Graphiques de performance
|
||||||
|
|
||||||
|
Sécurité (Fix P0-A) :
|
||||||
|
- HTTP Basic Auth sur tous les endpoints (middleware before_request).
|
||||||
|
- Credentials via DASHBOARD_USER / DASHBOARD_PASSWORD.
|
||||||
|
- Exceptions : /health, /healthz, /api/health (monitoring externe).
|
||||||
|
- Désactivation auth en dev local : DASHBOARD_AUTH_DISABLED=true
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import hmac
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
@@ -41,9 +49,125 @@ app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev-key-change-in-production
|
|||||||
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
|
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Fix P0-A : HTTP Basic Auth sur le dashboard (port 5001)
|
||||||
|
# =============================================================================
|
||||||
|
# Avant ce fix, 71 endpoints étaient exposés sans authentification.
|
||||||
|
# On ajoute un middleware Flask (before_request) qui exige un header
|
||||||
|
# Authorization: Basic <b64>. Les credentials sont pris dans l'environnement.
|
||||||
|
#
|
||||||
|
# Chemins publics (pas de challenge) : healthcheck uniquement — ils servent au
|
||||||
|
# monitoring externe (Prometheus, systemd, k8s, NPM reverse proxy).
|
||||||
|
#
|
||||||
|
# Pour désactiver l'auth (dev local, tests) : DASHBOARD_AUTH_DISABLED=true.
|
||||||
|
# Les tests unitaires définissent cette variable via un flag Flask config.
|
||||||
|
|
||||||
|
_DASHBOARD_USER = os.getenv("DASHBOARD_USER", "lea").strip()
|
||||||
|
_DASHBOARD_PASSWORD = os.getenv("DASHBOARD_PASSWORD", "").strip()
|
||||||
|
_DASHBOARD_AUTH_DISABLED = os.getenv("DASHBOARD_AUTH_DISABLED", "").lower() in (
|
||||||
|
"1", "true", "yes",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Si pas de password défini en env ET auth pas explicitement désactivée →
|
||||||
|
# on utilise un mot de passe par défaut "safe" (long, random-ish) ET on log
|
||||||
|
# un WARNING très visible au démarrage pour forcer Dom à le configurer
|
||||||
|
# avant un déploiement prod. On ne veut surtout pas générer un mot de passe
|
||||||
|
# aléatoire à chaque boot (même problème que l'API token auto-généré).
|
||||||
|
if not _DASHBOARD_PASSWORD and not _DASHBOARD_AUTH_DISABLED:
|
||||||
|
_DASHBOARD_PASSWORD = "changeme-dashboard-Medecin2026!"
|
||||||
|
api_logger.warning(
|
||||||
|
"[SÉCURITÉ] DASHBOARD_PASSWORD non défini en env — utilisation d'un "
|
||||||
|
"mot de passe par défaut temporaire. DÉFINIR DASHBOARD_PASSWORD "
|
||||||
|
"AVANT TOUT DÉPLOIEMENT (identifiant : DASHBOARD_USER)."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Paths publics (pas d'auth, pour healthchecks externes)
|
||||||
|
_PUBLIC_DASHBOARD_PATHS = {
|
||||||
|
"/health",
|
||||||
|
"/healthz",
|
||||||
|
"/api/health",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _dashboard_auth_ok(header_value: str) -> bool:
|
||||||
|
"""Valide le header Authorization Basic. Comparaison constant-time."""
|
||||||
|
if not header_value or not header_value.lower().startswith("basic "):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
decoded = base64.b64decode(header_value[6:].strip()).decode("utf-8")
|
||||||
|
except (ValueError, UnicodeDecodeError):
|
||||||
|
return False
|
||||||
|
if ":" not in decoded:
|
||||||
|
return False
|
||||||
|
user, _, password = decoded.partition(":")
|
||||||
|
# Comparaison constant-time pour éviter les timing attacks.
|
||||||
|
user_ok = hmac.compare_digest(user, _DASHBOARD_USER)
|
||||||
|
pwd_ok = hmac.compare_digest(password, _DASHBOARD_PASSWORD)
|
||||||
|
return user_ok and pwd_ok
|
||||||
|
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def _dashboard_basic_auth_middleware():
|
||||||
|
"""Middleware d'auth HTTP Basic sur tous les endpoints HTTP du dashboard.
|
||||||
|
|
||||||
|
- Bypass complet si DASHBOARD_AUTH_DISABLED=true (dev/tests).
|
||||||
|
- Bypass complet si app.config['TESTING'] (pytest) et qu'aucun credential
|
||||||
|
n'est passé : les tests existants du dashboard doivent continuer de
|
||||||
|
passer sans retoucher chaque fixture.
|
||||||
|
- Paths dans _PUBLIC_DASHBOARD_PATHS : toujours publics (healthchecks).
|
||||||
|
- Sinon : header Authorization: Basic <b64> obligatoire.
|
||||||
|
|
||||||
|
Note WebSocket : Flask-SocketIO utilise son propre canal pour le handshake.
|
||||||
|
Le before_request ci-dessus s'applique à la requête HTTP de l'upgrade
|
||||||
|
(compatible mode threading). Les sockets post-handshake ne passent pas par
|
||||||
|
Flask, c'est acceptable pour un MVP (le client doit avoir passé l'auth HTTP).
|
||||||
|
"""
|
||||||
|
# Dev / tests : bypass total
|
||||||
|
if _DASHBOARD_AUTH_DISABLED:
|
||||||
|
return None
|
||||||
|
if app.config.get("TESTING") and not app.config.get("DASHBOARD_AUTH_ENABLED"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
path = request.path or "/"
|
||||||
|
if path in _PUBLIC_DASHBOARD_PATHS:
|
||||||
|
return None
|
||||||
|
|
||||||
|
header_value = request.headers.get("Authorization", "")
|
||||||
|
if _dashboard_auth_ok(header_value):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Pas authentifié — challenge 401 avec WWW-Authenticate
|
||||||
|
return Response(
|
||||||
|
'{"error": "authentication required"}',
|
||||||
|
status=401,
|
||||||
|
mimetype="application/json",
|
||||||
|
headers={"WWW-Authenticate": 'Basic realm="RPA Vision V3 Dashboard"'},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get('/healthz')
|
@app.get('/healthz')
|
||||||
def healthz():
|
def healthz():
|
||||||
"""Healthcheck minimal (systemd/k8s)."""
|
"""Healthcheck minimal (systemd/k8s). Public — pas d'auth."""
|
||||||
|
return jsonify({
|
||||||
|
'status': 'ok',
|
||||||
|
'service': 'rpa-vision-v3-dashboard',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get('/api/health')
|
||||||
|
def api_health():
|
||||||
|
"""Healthcheck JSON public — pas d'auth (monitoring externe)."""
|
||||||
|
return jsonify({
|
||||||
|
'status': 'ok',
|
||||||
|
'service': 'rpa-vision-v3-dashboard',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get('/health')
|
||||||
|
def health():
|
||||||
|
"""Healthcheck public — pas d'auth (NPM reverse proxy)."""
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'ok',
|
'status': 'ok',
|
||||||
'service': 'rpa-vision-v3-dashboard',
|
'service': 'rpa-vision-v3-dashboard',
|
||||||
@@ -532,21 +656,29 @@ def get_session(session_id):
|
|||||||
|
|
||||||
@app.route('/api/agent/sessions/<session_id>/screenshot/<filename>')
|
@app.route('/api/agent/sessions/<session_id>/screenshot/<filename>')
|
||||||
def get_screenshot(session_id, filename):
|
def get_screenshot(session_id, filename):
|
||||||
"""Récupère un screenshot."""
|
"""Récupère un screenshot.
|
||||||
|
|
||||||
|
Par défaut, si une version floutée (PII) `<stem>_blurred.png` existe à côté
|
||||||
|
du fichier demandé, elle est servie à la place (affichage conforme RGPD).
|
||||||
|
Pour obtenir la version brute, passer `?raw=1` (réservé aux endpoints
|
||||||
|
d'entraînement/grounding, à protéger par auth).
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
|
want_raw = request.args.get('raw', '0') in ('1', 'true', 'yes')
|
||||||
|
|
||||||
# Chercher le screenshot dans tous les répertoires
|
# Chercher le screenshot dans tous les répertoires
|
||||||
screenshot_path = None
|
screenshot_path = None
|
||||||
|
|
||||||
for dir_path in SESSIONS_PATH.iterdir():
|
for dir_path in SESSIONS_PATH.iterdir():
|
||||||
if not dir_path.is_dir():
|
if not dir_path.is_dir():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Chercher dans différents emplacements possibles
|
# Chercher dans différents emplacements possibles
|
||||||
possible_paths = [
|
possible_paths = [
|
||||||
dir_path / "screenshots" / filename,
|
dir_path / "screenshots" / filename,
|
||||||
dir_path / "shots" / filename,
|
dir_path / "shots" / filename,
|
||||||
]
|
]
|
||||||
|
|
||||||
# Chercher aussi dans les sous-répertoires
|
# Chercher aussi dans les sous-répertoires
|
||||||
for subdir in dir_path.iterdir():
|
for subdir in dir_path.iterdir():
|
||||||
if subdir.is_dir():
|
if subdir.is_dir():
|
||||||
@@ -554,18 +686,26 @@ def get_screenshot(session_id, filename):
|
|||||||
subdir / "screenshots" / filename,
|
subdir / "screenshots" / filename,
|
||||||
subdir / "shots" / filename,
|
subdir / "shots" / filename,
|
||||||
])
|
])
|
||||||
|
|
||||||
for path in possible_paths:
|
for path in possible_paths:
|
||||||
if path.exists():
|
if path.exists():
|
||||||
screenshot_path = path
|
screenshot_path = path
|
||||||
break
|
break
|
||||||
|
|
||||||
if screenshot_path:
|
if screenshot_path:
|
||||||
break
|
break
|
||||||
|
|
||||||
if not screenshot_path:
|
if not screenshot_path:
|
||||||
return jsonify({'error': 'Screenshot non trouvé'}), 404
|
return jsonify({'error': 'Screenshot non trouvé'}), 404
|
||||||
|
|
||||||
|
# Préférer la version floutée si dispo et si l'appelant ne demande pas le brut
|
||||||
|
if not want_raw and "_blurred" not in screenshot_path.stem:
|
||||||
|
blurred_candidate = screenshot_path.with_name(
|
||||||
|
f"{screenshot_path.stem}_blurred{screenshot_path.suffix}"
|
||||||
|
)
|
||||||
|
if blurred_candidate.is_file():
|
||||||
|
screenshot_path = blurred_candidate
|
||||||
|
|
||||||
return send_file(screenshot_path, mimetype='image/png')
|
return send_file(screenshot_path, mimetype='image/png')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
@@ -1630,6 +1770,14 @@ SERVICES_CONFIG = {
|
|||||||
"url": "http://localhost:5005",
|
"url": "http://localhost:5005",
|
||||||
"icon": "📡"
|
"icon": "📡"
|
||||||
},
|
},
|
||||||
|
"session_cleaner": {
|
||||||
|
"name": "Session Cleaner",
|
||||||
|
"description": "Nettoyage de sessions avant replay (dépend du Streaming Server)",
|
||||||
|
"port": 5006,
|
||||||
|
"start_cmd": "cd {base} && {base}/.venv/bin/python3 tools/session_cleaner.py",
|
||||||
|
"url": "http://localhost:5006",
|
||||||
|
"icon": "🧹"
|
||||||
|
},
|
||||||
"web_dashboard": {
|
"web_dashboard": {
|
||||||
"name": "Dashboard (ce service)",
|
"name": "Dashboard (ce service)",
|
||||||
"description": "Panneau de contrôle RPA Vision V3",
|
"description": "Panneau de contrôle RPA Vision V3",
|
||||||
|
|||||||
@@ -69,6 +69,7 @@
|
|||||||
<div class="tab" onclick="switchTab('corrections')">🔧 Corrections</div>
|
<div class="tab" onclick="switchTab('corrections')">🔧 Corrections</div>
|
||||||
<div class="tab" onclick="switchTab('learning')">🧠 Apprentissage</div>
|
<div class="tab" onclick="switchTab('learning')">🧠 Apprentissage</div>
|
||||||
<div class="tab" onclick="switchTab('config')">🔧 Configuration</div>
|
<div class="tab" onclick="switchTab('config')">🔧 Configuration</div>
|
||||||
|
<div class="tab" onclick="switchTab('cleaner')">🧹 Nettoyage</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -783,6 +784,14 @@
|
|||||||
<button class="btn btn-small btn-primary" onclick="testConnection('service', 'upload_api')">Test</button>
|
<button class="btn btn-small btn-primary" onclick="testConnection('service', 'upload_api')">Test</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="config-item">
|
||||||
|
<label>Session Cleaner (port 5006)</label>
|
||||||
|
<div style="display:flex;gap:10px;">
|
||||||
|
<input type="text" id="cfg_session_cleaner_host" placeholder="localhost" class="config-input" style="flex:2;">
|
||||||
|
<input type="number" id="cfg_session_cleaner_port" placeholder="5006" class="config-input" style="flex:1;">
|
||||||
|
<button class="btn btn-small btn-primary" onclick="testConnection('service', 'session_cleaner')">Test</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -834,37 +843,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-2">
|
<div class="grid grid-2">
|
||||||
<!-- Section Detection -->
|
|
||||||
<div class="card">
|
|
||||||
<h2><span class="icon">👁️</span> Detection Visuelle</h2>
|
|
||||||
<div id="configDetection" class="config-section">
|
|
||||||
<div class="config-item">
|
|
||||||
<label>Modele OWL</label>
|
|
||||||
<select id="cfg_detection_owl_model" class="config-input">
|
|
||||||
<option value="google/owlv2-base-patch16-ensemble">OWLv2 Base Ensemble</option>
|
|
||||||
<option value="google/owlv2-large-patch14-ensemble">OWLv2 Large Ensemble</option>
|
|
||||||
<option value="google/owlvit-base-patch32">OWLViT Base</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="config-item">
|
|
||||||
<label>Seuil de confiance</label>
|
|
||||||
<input type="range" id="cfg_detection_confidence" min="0.1" max="0.9" step="0.05" value="0.3" class="config-input" oninput="document.getElementById('confValue').textContent = this.value">
|
|
||||||
<span id="confValue" style="color:#3b82f6;margin-left:10px;">0.3</span>
|
|
||||||
</div>
|
|
||||||
<div class="config-item">
|
|
||||||
<label>Seuil NMS</label>
|
|
||||||
<input type="range" id="cfg_detection_nms" min="0.1" max="0.9" step="0.05" value="0.3" class="config-input" oninput="document.getElementById('nmsValue').textContent = this.value">
|
|
||||||
<span id="nmsValue" style="color:#3b82f6;margin-left:10px;">0.3</span>
|
|
||||||
</div>
|
|
||||||
<div class="config-item">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" id="cfg_detection_use_gpu" checked>
|
|
||||||
Utiliser GPU (CUDA)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Section Base de donnees -->
|
<!-- Section Base de donnees -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2><span class="icon">💾</span> Base de Donnees</h2>
|
<h2><span class="icon">💾</span> Base de Donnees</h2>
|
||||||
@@ -896,9 +874,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-2">
|
|
||||||
<!-- Section Securite -->
|
<!-- Section Securite -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2><span class="icon">🔒</span> Securite</h2>
|
<h2><span class="icon">🔒</span> Securite</h2>
|
||||||
@@ -963,6 +939,46 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab: Nettoyage de sessions (iframe vers session_cleaner port 5006) -->
|
||||||
|
<div id="tab-cleaner" class="tab-content">
|
||||||
|
<div class="card" style="margin-bottom:20px;background:linear-gradient(135deg, #1e293b 0%, #0f172a 100%);">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:15px;">
|
||||||
|
<div>
|
||||||
|
<h2 style="margin-bottom:5px;color:#e2e8f0;"><span class="icon">🧹</span> Nettoyage de sessions avant replay</h2>
|
||||||
|
<p style="color:#64748b;font-size:13px;">Visualisez les sessions, supprimez les clics parasites et regénérez un replay propre (Session Cleaner, port 5006)</p>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:10px;">
|
||||||
|
<button class="btn btn-primary" onclick="refreshCleanerFrame()">🔄 Recharger</button>
|
||||||
|
<a class="btn btn-secondary" href="http://localhost:5006" target="_blank" rel="noopener">↗ Ouvrir dans un onglet</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Message d'état (affiché si le service n'est pas démarré) -->
|
||||||
|
<div id="cleanerOfflineNotice" class="card" style="display:none;margin-bottom:20px;border:1px solid #ef4444;">
|
||||||
|
<div style="display:flex;align-items:center;gap:20px;flex-wrap:wrap;">
|
||||||
|
<div style="font-size:48px;">⚠️</div>
|
||||||
|
<div style="flex:1;min-width:250px;">
|
||||||
|
<h3 style="color:#ef4444;margin-bottom:8px;">Session Cleaner non démarré</h3>
|
||||||
|
<p style="color:#94a3b8;font-size:13px;">Le service sur le port 5006 ne répond pas. Démarrez-le pour accéder à l'interface de nettoyage.</p>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:10px;">
|
||||||
|
<button class="btn btn-success" onclick="startCleanerService()" id="btnStartCleaner">▶️ Démarrer le cleaner</button>
|
||||||
|
<button class="btn btn-secondary" onclick="switchTab('services')">🎛️ Gérer les services</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- iframe vers le cleaner -->
|
||||||
|
<div id="cleanerFrameContainer" class="card" style="padding:0;overflow:hidden;">
|
||||||
|
<iframe
|
||||||
|
id="cleanerFrame"
|
||||||
|
src="about:blank"
|
||||||
|
style="width:100%;height:85vh;min-height:800px;border:0;border-radius:12px;background:#0f172a;"
|
||||||
|
title="Session Cleaner"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -1199,6 +1215,73 @@
|
|||||||
if (tabName === 'corrections') refreshCorrectionPacks();
|
if (tabName === 'corrections') refreshCorrectionPacks();
|
||||||
if (tabName === 'learning') refreshLearningStats();
|
if (tabName === 'learning') refreshLearningStats();
|
||||||
if (tabName === 'config') refreshConfig();
|
if (tabName === 'config') refreshConfig();
|
||||||
|
if (tabName === 'cleaner') checkCleanerStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Session Cleaner (iframe vers port 5006) ===
|
||||||
|
const CLEANER_URL = 'http://localhost:5006';
|
||||||
|
|
||||||
|
async function checkCleanerStatus() {
|
||||||
|
const notice = document.getElementById('cleanerOfflineNotice');
|
||||||
|
const frameContainer = document.getElementById('cleanerFrameContainer');
|
||||||
|
const frame = document.getElementById('cleanerFrame');
|
||||||
|
if (!notice || !frameContainer || !frame) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/services/session_cleaner/status');
|
||||||
|
const data = await res.json();
|
||||||
|
const running = data && data.status === 'running';
|
||||||
|
|
||||||
|
if (running) {
|
||||||
|
notice.style.display = 'none';
|
||||||
|
frameContainer.style.display = 'block';
|
||||||
|
// Charger l'iframe seulement si ce n'est pas déjà fait
|
||||||
|
if (frame.src === 'about:blank' || !frame.src.startsWith(CLEANER_URL)) {
|
||||||
|
frame.src = CLEANER_URL;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notice.style.display = 'block';
|
||||||
|
frameContainer.style.display = 'none';
|
||||||
|
frame.src = 'about:blank';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('checkCleanerStatus error:', err);
|
||||||
|
notice.style.display = 'block';
|
||||||
|
frameContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshCleanerFrame() {
|
||||||
|
const frame = document.getElementById('cleanerFrame');
|
||||||
|
if (!frame) return;
|
||||||
|
// Forcer un rechargement (cache busting)
|
||||||
|
frame.src = CLEANER_URL + '?t=' + Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startCleanerService() {
|
||||||
|
const btn = document.getElementById('btnStartCleaner');
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '⏳ Démarrage...';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/services/session_cleaner/start', { method: 'POST' });
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) {
|
||||||
|
alert('Erreur : ' + (data.error || 'démarrage impossible'));
|
||||||
|
} else {
|
||||||
|
// Laisser le temps au service de démarrer
|
||||||
|
await new Promise(r => setTimeout(r, 1500));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert('Erreur réseau : ' + err.message);
|
||||||
|
} finally {
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = '▶️ Démarrer le cleaner';
|
||||||
|
}
|
||||||
|
await checkCleanerStatus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update execution UI
|
// Update execution UI
|
||||||
@@ -2852,15 +2935,7 @@
|
|||||||
refreshOllamaModels();
|
refreshOllamaModels();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detection
|
// Detection (OWL-v2 legacy) — section UI retiree, config preservee telle quelle
|
||||||
if (config.detection) {
|
|
||||||
document.getElementById('cfg_detection_owl_model').value = config.detection.owl_model || 'google/owlv2-base-patch16-ensemble';
|
|
||||||
document.getElementById('cfg_detection_confidence').value = config.detection.confidence_threshold || 0.3;
|
|
||||||
document.getElementById('confValue').textContent = config.detection.confidence_threshold || 0.3;
|
|
||||||
document.getElementById('cfg_detection_nms').value = config.detection.nms_threshold || 0.3;
|
|
||||||
document.getElementById('nmsValue').textContent = config.detection.nms_threshold || 0.3;
|
|
||||||
document.getElementById('cfg_detection_use_gpu').checked = config.detection.use_gpu !== false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
if (config.database) {
|
if (config.database) {
|
||||||
@@ -2936,12 +3011,14 @@
|
|||||||
model: document.getElementById('cfg_vlm_model').value,
|
model: document.getElementById('cfg_vlm_model').value,
|
||||||
description: 'Modele VLM pour l\'analyse visuelle'
|
description: 'Modele VLM pour l\'analyse visuelle'
|
||||||
},
|
},
|
||||||
detection: {
|
// Detection: section UI retiree (OWL-v2 remplace par pipeline VLM).
|
||||||
owl_model: document.getElementById('cfg_detection_owl_model').value,
|
// On preserve la config existante pour le fallback eventuel.
|
||||||
confidence_threshold: parseFloat(document.getElementById('cfg_detection_confidence').value),
|
detection: currentConfig.detection || {
|
||||||
nms_threshold: parseFloat(document.getElementById('cfg_detection_nms').value),
|
owl_model: 'google/owlv2-base-patch16-ensemble',
|
||||||
use_gpu: document.getElementById('cfg_detection_use_gpu').checked,
|
confidence_threshold: 0.3,
|
||||||
description: 'Configuration du detecteur visuel OWL-v2'
|
nms_threshold: 0.3,
|
||||||
|
use_gpu: true,
|
||||||
|
description: 'Configuration legacy du detecteur visuel OWL-v2 (fallback)'
|
||||||
},
|
},
|
||||||
embedding: currentConfig.embedding || {
|
embedding: currentConfig.embedding || {
|
||||||
model: 'clip',
|
model: 'clip',
|
||||||
|
|||||||
Reference in New Issue
Block a user