docs: track design docs, plans, audits, coordination infrastructure, handoffs

- 21 docs/*.md: audits, design notes, deployment plans, checklists, memos
- Coordination: ROLES, runbooks (DGX reboot, Lea live), patches, registre, syntheses, systemd, QG template
- Handoffs: 6 Codex handoff documents + README + template
This commit is contained in:
Dom
2026-07-02 13:29:58 +02:00
parent 7dd5c872df
commit 6907ecc82f
42 changed files with 5267 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
# Rôles — Coordination multi-agents rpa_vision_v3
- `Date`: 2026-06-08
- `Auteur`: Qwen
- `Statut`: actif
---
## Équipe
| Agent | Rôle | Inbox | Sortie attendue |
|---|---|---|---|
| **Dom** | Propriétaire du projet, décideur final, filtre produit | Direct | GO/NOGO, arbitrages, validation démo |
| **Codex** | Coordinateur, orchestration, arbitrages techniques, synthèse | `inbox_codex/` | Missions, QG consolidés, plans, décisions |
| **Claude** | Implémentation, patches, benchmarks, plans techniques | `inbox_claude/` | Commits, RESULTAT, plans, analyses |
| **Qwen** | Quality Gate (QG), audit, historien, garde-fou, propositions | `inbox_qwen/` | Verdicts GO/NOGO, audits, synthèses, docs |
| **Gemini** | Consultation ponctuelle, recherche externe | `inbox_gemini/` | Recherche, analyse comparative |
---
## Règles de coordination
### 1. Inbox routing
- 1 question = 1 fichier dans l'inbox du destinataire
- Format : `YYYY-MM-DD_HHMM_auteur-to-destinataire_SUJET.md`
- Réponse courte et actionnable, pas de rapport long sauf demande explicite
### 1bis. Orchestration Codex
- Codex est l'orchestrateur actif du projet.
- Tant que Claude et Qwen ont des loops de coordination actifs, Codex doit leur donner en continu une prochaine tâche actionnable.
- Exception unique : Codex attend explicitement un feu vert de Dom ou un QG bloquant avant d'autoriser l'étape suivante.
- Si une exécution est bloquée, Codex doit quand même distribuer du travail non destructif utile : préparation, QG, audit read-only, rollback, plan de tests, registre décisions.
- Claude ne doit pas rester sans mission d'exécution/preuves ou préparation technique.
- Qwen ne doit pas rester sans mission de QG, audit, contradiction check ou registre.
### 2. Statuts
| Statut | Signification |
|---|---|
| `open` | En attente de réponse |
| `ACK` | Lu et pris en compte |
| `RESULTAT` | Livraison d'un résultat |
| `GO` / `NO-GO` | Verdict qualité |
| `DRAFT` | Brouillon, pas encore prêt pour revue |
| `archived` | Archivé (ne pas supprimer) |
### 3. Règle QG
- Pas d'action structurelle sans QG (Quality Gate) explicite
- Pas de promotion de competence sans QG + GO Dom
- Qwen signale à Dom toute contradiction entre agents
### 4. Règles de sécurité
- Pas de secret en clair dans les messages de coordination
- Pas de replay autonome sans Dom devant Windows
- Pas de code mort supprimé sans GO Dom explicite
- Pas d'activation modèle/grounder sans bench + QG + GO Dom
### 5. Capitalisation
- Chaque avancée importante doit être capitalisée dans `active/` ou `registre/`
- Les inboxes sont un flux, pas une mémoire
- `syntheses/` = rapports d'inventaires et benchmarks
- `registre/` = décisions formelles
### 6. Sessions
- Qwen reste dans la session longue (historien, garde le fil complet)
- Codex et Claude ont chacun leur session propre
- Si Qwen voit des contradictions entre les instructions de Codex/Claude, il signale à Dom avant d'agir
---
## Flux de travail typique
```
Codex → mission → Claude/ Qwen
Claude → implémentation → commit → RESULTAT → Codex/Qwen
Qwen → QG → GO/NO-GO → Codex
Codex → synthèse → Dom → GO/NOGO
```
---
*Document vivant — mettre à jour quand les rôles évoluent.*

View File

@@ -0,0 +1,233 @@
# RUNBOOK DGX - Check post-deploiement et post-reboot POC
- Date: 2026-06-17
- Responsable coordination: Codex
- Scope: POC DGX uniquement
- Cible labo actuelle: DGX `192.168.1.45`
- Cible clinique preparee: Ethernet `192.168.1.178/24`, gateway `192.168.1.243`, DNS `192.168.1.9` puis `192.168.1.8`, IPv6 inactive, profil inactif tant que Dom/Codex ne donnent pas le GO.
## Objectif
Valider qu'apres deploiement puis redemarrage du DGX, toute la chaine POC reste fonctionnelle sans dependance `localhost` cote utilisateur:
- dashboard;
- VWB;
- import workflow;
- anchors visibles;
- agent-chat / "Discuter avec Lea";
- streaming/fleet;
- worker/replay;
- installateur Lea autonome;
- configuration reseau DGX sans bascule clinique accidentelle.
## Regle de conduite
- Dom execute uniquement les gestes UI de la section "Check Dom".
- Les agents techniques collectent les preuves systeme et reseau.
- Toute divergence est notee avec: etape, symptome, impact, rollback propose.
- Pas de changement reseau clinique actif pendant le check labo: le Wi-Fi reste le lien de test.
## 0. Prerequis avant reboot
| Point | Attendu | Statut |
|---|---|---|
| Branche DGX | `poc-dgx` | A confirmer par Claude |
| Commit DGX | `9605cc9d9` ou commit ulterieur explicitement valide | A confirmer par Claude |
| Commits POC presents | `667575c3a`, `9605cc9d9` | A confirmer par Claude/Qwen |
| Deploy propre | pas de fichier parasite embarque dans le runtime POC | A confirmer |
| Donnees runtime | backup `backend/instance/workflows.db` avant reset/deploy, restore apres deploy, hash/size verifies | A confirmer par Claude |
| Donnees non trackees | `data/workflows` (symlink) + donnees VWB sous `visual_workflow_builder/backend/data/` non supprimes | A confirmer |
| VWB build | bundle reconstruit depuis `poc-dgx` | A confirmer |
| Installateur Lea | version `1.0.1`, Python embedded obligatoire | A confirmer |
| Profil Ethernet clinique | prepare si besoin, `autoconnect off`, non active | A confirmer par Qwen |
| Enrolement VM Lea | VM/poste pointe vers DGX labo `192.168.1.45` pour le test, pas vers l'URL cloud | A confirmer |
NO-GO reboot si le deploiement n'est pas fige, si le DGX ne pointe pas sur le bon commit, ou si `workflows.db`/anchors/workflows ne sont pas verifies apres deploy.
## 1. Baseline avant reboot
Un agent technique releve l'etat avant redemarrage:
| Famille | Controle | Attendu |
|---|---|---|
| Services principaux | dashboard, VWB backend, streaming, agent-chat, worker/fleet actifs | actifs |
| Ports utilisateur | `5001`, `5004`, `5005` disponibles depuis le LAN autorise | OK |
| Ports internes | `5002`, `8000`, `11434` selon architecture DGX | OK ou documente |
| Dashboard | acces par `http://192.168.1.45:5001` | login ou 401 attendu, pas page blanche |
| Streaming | health `:5005` | 200/healthy |
| Auth streaming | route protegee sans token | 401 attendu |
| Agent chat | status/health `:5004` | 200/online |
| VWB | frontend charge depuis IP DGX | OK |
| Bundle VWB | aucune URL active `localhost`/`127.0.0.1` | OK |
| VM Windows Lea | Lea pointe vers DGX `192.168.1.45` pour `5004/5005` | OK |
Si un point est deja rouge avant reboot, on corrige avant de redemarrer. Le reboot ne doit pas servir a masquer une panne.
## 2. Reboot DGX
1. Claude annonce le debut du reboot dans la coordination.
2. Codex garde la coordination ouverte.
3. Attendre le retour reseau du DGX sur le Wi-Fi labo.
4. Ne pas activer le profil Ethernet clinique pendant ce cycle.
5. Claude poste l'heure de retour et le premier etat des services.
NO-GO immediat si le DGX ne revient pas sur le reseau labo ou si le dashboard reste inaccessible plus de 10 minutes apres retour reseau.
## 3. Check technique post-reboot
### 3.1 Services
Noms constates/attendus a verifier sur le DGX. Ne pas supposer qu'un service existe: si une unite est absente, le noter et verifier si sa fonction est couverte par un autre service.
- `rpa-streaming`
- `rpa-agent-chat`
- `rpa-vision-v3-dashboard`
- `rpa-vision-v3-vwb-frontend`
- `rpa-vision-v3-vwb-backend`
- `rpa-vision-v3-worker`
- `rpa-vision-v3-stream-worker` si installe, sinon documenter l'absence
- `rpa-vision-v3-api`
- `rpa-grounding` ou service grounder equivalent si present; sinon documenter le fallback
- `rpa-vision-v3-healthcheck.timer` si actif dans le deploiement final; sinon ne pas bloquer mais documenter
Attendu:
- services POC critiques actifs apres reboot;
- services POC critiques enabled si necessaires au demarrage automatique;
- pas de boucle restart;
- logs recents sans traceback bloquant;
- healthcheck timer actif ou decision documentee si non utilise;
- VWB frontend servi par le vrai frontend POC (`frontend_v4`), pas l'ancien `frontend/`.
- Ne jamais utiliser `git clean -xfd` sur DGX pendant la sequence POC: cela detruirait les donnees runtime non trackees.
### 3.2 Reseau et ports
| Port | Usage | Attendu labo |
|---|---|---|
| `5001` | dashboard / VWB integre | accessible via `192.168.1.45` |
| `5004` | agent-chat / ChatWindow Lea | accessible depuis VM Windows autorisee |
| `5005` | streaming/fleet/replay | accessible depuis VM Windows autorisee |
| `5002` | VWB backend si separe | conforme au deploiement DGX |
| `8000` | upload/API core si actif | conforme au deploiement DGX |
| `11434` | Ollama local DGX | local/interne sauf decision contraire |
Attendu:
- aucun appel navigateur produit vers `localhost` ou `127.0.0.1`;
- `5004/5005` repondent depuis la VM Windows;
- le dashboard charge toutes ses ressources depuis l'IP DGX;
- la carte Ethernet clinique reste inactive ou non connectee pendant le check labo.
### 3.3 Endpoints et fonctions serveur
| Controle | Attendu |
|---|---|
| Dashboard `5001` | page login/auth visible, pas 500 |
| Streaming `5005/health` | healthy |
| Streaming route protegee sans token | 401 |
| Fleet machines | VM Lea visible ou reenrolement documente |
| Enrolement VM Lea | `config.txt`/runtime pointe vers DGX, pas vers URL cloud |
| Replay next | repond correctement pour machine enrollee |
| Agent chat `5004` | online et session chat possible |
| VWB catalog/workflows | liste chargee |
| Anchors API | upload/thumbnail/original accessibles depuis IP DGX |
| Worker | pas de backlog bloquant, pas de crash loop |
| Modele/grounding | service disponible ou fallback documente |
## 4. Check Dom UI post-reboot
Dom ne fait que ces gestes:
| Etape | Geste Dom | GO | NO-GO |
|---|---|---|---|
| T1 Dashboard | Ouvrir l'URL DGX fournie | page login/dashboard visible | page blanche, site inaccessible, erreur 500 |
| T2 Chat Lea | Dans la VM, ouvrir "Discuter avec Lea", envoyer "Bonjour" | reponse en quelques secondes | pas connectee, erreur serveur, fermeture fenetre |
| T3 Bulles action | Demander une action simple | bulles/progression visibles | aucune progression visible pendant une action lancee |
| T4 Import VWB | Importer un workflow JSON Lea | succes, workflow visible | erreur, rien ne se passe |
| T5 Anchors | Ouvrir une etape avec ancre | image d'ancre/crop visible | image absente, plein ecran au lieu de crop |
| T6 Replay supervise | Lancer un replay safe/supervise | execution ou demande de confirmation | clic hors cible, blocage, absence de reaction |
| T7 Reconnexion apres reboot | Fermer/rouvrir Lea sur VM | reconnecte DGX sans reconfig manuelle | demande Python/outils externes ou config manuelle non prevue |
## 5. Criteres GO/NO-GO
GO livraison labo si:
- DGX revient apres reboot;
- services critiques actifs;
- dashboard accessible via IP DGX;
- VWB charge sans `localhost`;
- chat Lea fonctionne depuis VM;
- import workflow OK;
- anchors visibles;
- replay supervise OK ou comportement d'arret/confirmation conforme;
- installateur Lea autonome confirme;
- profil Ethernet clinique non active par erreur.
NO-GO livraison si un des points suivants echoue:
- DGX ne revient pas proprement apres reboot;
- dashboard/VWB inaccessible;
- `5004` ou `5005` indisponible depuis la VM;
- "Discuter avec Lea" ne fonctionne pas;
- installateur demande Python ou un outil externe;
- VWB appelle encore `localhost` cote navigateur;
- import/anchors/replay cassent;
- configuration Ethernet clinique activee accidentellement pendant les tests labo.
## 6. Preuves a archiver
Claude fournit:
- commit DGX courant;
- preuve backup/restore `workflows.db` si reset/deploy effectue;
- confirmation que `data/workflows` et les vrais chemins `visual_workflow_builder/backend/data/anchors`, `visual_workflow_builder/backend/data/anchor_images`, `visual_workflow_builder/backend/instance/workflows.db` sont presents apres deploy/reboot;
- liste services actifs/enabled;
- ports et endpoints avec resultats;
- resultat grep bundle no-localhost;
- resultat test VM Lea chat;
- resultat import/anchors/replay;
- chemin de l'artefact installateur Lea 1.0.1;
- incidents et rollback si besoin.
Qwen fournit:
- controle croise commits/fichiers parasites;
- matrice GO/NO-GO post-reboot;
- verification profil Ethernet clinique inactif;
- etat VM Windows 11 ARM DGX ou decision fallback VMware clinique.
Codex consolide:
- verdict final pret/pas pret;
- liste des anomalies restantes;
- decision de passage au check site ou retour correction.
## 7. Rollback minimal
Si VWB regresse:
- revenir au dernier commit POC valide connu;
- redeployer le bundle precedent;
- redemarrer uniquement dashboard/VWB backend;
- refaire T1, T4, T5.
Si agent-chat regresse:
- verifier service `rpa-agent-chat`;
- verifier env `5004`/feedback bus;
- redemarrer agent-chat puis streaming si necessaire;
- refaire T2/T3.
Si streaming/fleet regresse:
- verifier service `rpa-streaming`;
- verifier token/auth;
- verifier reenrolement VM;
- refaire T2/T6.
Si reseau clinique s'active par erreur:
- revenir au profil Wi-Fi labo;
- desactiver le profil Ethernet clinique;
- confirmer retour dashboard `192.168.1.45`;
- ne reprendre les tests qu'apres stabilisation.

View File

@@ -0,0 +1,97 @@
# Runbook — Test Lea live grandeur nature
- `Date`: 2026-06-08
- `Auteur`: Qwen (draft)
- `Statut`: DRAFT — en attente preflight vert + GO Dom
- **Interdit** : ❌ Replay autonome
---
## Prérequis
| # | Vérification | Commande / Action | Critère GO |
|---|---|---|---|
| 1 | Windows cible visible | `ping DESKTOP-58D5CAC_windows` | ✅ Répond |
| 2 | httpx OK côté Windows | `python -c "import httpx; print('OK')"` dans venv Windows | ✅ Import OK |
| 3 | Agent V1 en cours | Vérifier processus Windows agent V1 | ✅ Running |
| 4 | DGX Ollama accessible | `curl http://localhost:11434/api/tags` | ✅ Répond |
| 5 | Workflows acquis retrouvés | Dashboard → workflows chargés | ✅ ≥ 1 workflow visible |
| 6 | Dom présent physiquement | Devant le poste Windows | ✅ Confirmé |
---
## Exécution
### Étape 1 : Préflight (NON destructif)
```bash
# Sur le poste dev Linux
curl -X POST http://localhost:5005/api/v1/traces/stream/replay/preflight \
-H "Content-Type: application/json" \
-d '{"machine_id": "DESKTOP-58D5CAC_windows"}'
```
**Attendu** : `workflow_known: true`, aucun blocage G1-G6.
### Étape 2 : Capture Shadow supervisée
1. Ouvrir le dashboard Lea
2. Démarrer un apprentissage supervisé
3. Scénario safe : Notepad → Enregistrer → Explorateur → Easily Assure
4. **Ne pas** déclencher de replay autonome
5. Observer et collecter les preuves en temps réel
### Étape 3 : Collecte des preuves
| Preuve | Où la trouver | Critère |
|---|---|---|
| `live_events.jsonl` | `data/training/live_events.jsonl` | Non vide, timestamps cohérents |
| `learn_*.json` | `agent_chat/state/learn_*.json` | Présent, state non vide |
| Shadow understanding | `agent_chat/state/` | Compréhension non vide |
| Screenshots avant/après | Captures manuelles | Chaque action documentée |
| Rapports preflight | Sortie curl étape 1 | `workflow_known: true` |
### Étape 4 : Arrêt propre
1. Stopper l'apprentissage via le dashboard
2. Vérifier que les fichiers de state sont cohérents
3. Ne pas lancer de replay automatique
---
## Critères GO/NOGO
### GO (tous doivent être verts)
| ID | Critère |
|---|---|
| G1 | Préflight `workflow_known: true` |
| G2 | Windows cible joignable |
| G3 | `httpx` import OK côté Windows |
| G4 | DGX Ollama accessible (`localhost:11434`) |
| G5 | Workflows acquis retrouvés (≥ 1) |
| G6 | Dom confirme présent physiquement |
### NOGO (un seul suffit)
| ID | Critère |
|---|---|
| N1 | Préflight KO |
| N2 | Windows injoignable |
| N3 | `httpx` absent |
| N4 | DGX Ollama inaccessible |
| N5 | Dom absent devant Windows |
| N6 | Replay autonome demandé |
---
## Rollback
1. Arrêter proprement l'apprentissage
2. Ne pas supprimer les fichiers de state
3. Documenter le point d'arrêt
4. Rapporter à Codex/Claude
---
*Ce runbook est un draft. Il sera validé après preflight vert et GO Dom.*

View File

@@ -0,0 +1,208 @@
diff --git a/tests/unit/test_dashboard_routes.py b/tests/unit/test_dashboard_routes.py
index 3f8f0528c..69cc1b2fb 100644
--- a/tests/unit/test_dashboard_routes.py
+++ b/tests/unit/test_dashboard_routes.py
@@ -212,6 +212,58 @@ class TestDashboardRoutes:
data = resp.get_json()
assert 'workflows' in data
+ def test_workflows_list_reads_vwb_db(self, client, monkeypatch, tmp_path):
+ """Régression red-gate : /api/workflows reflète la base VWB v3, pas 0.
+
+ Avant correctif l'endpoint globait un store JSON vide et renvoyait
+ toujours total:0. On construit une DB VWB minimale (schéma canonique
+ workflows + steps) et on vérifie que l'endpoint expose le compte réel.
+ """
+ import sqlite3
+ from pathlib import Path
+
+ db_path = tmp_path / "instance" / "workflows.db"
+ db_path.parent.mkdir(parents=True, exist_ok=True)
+ conn = sqlite3.connect(str(db_path))
+ conn.execute(
+ "CREATE TABLE workflows (id VARCHAR(64) PRIMARY KEY, name VARCHAR(255), "
+ "description TEXT, created_at DATETIME, updated_at DATETIME, "
+ "is_active BOOLEAN, source VARCHAR(64), review_status VARCHAR(32))"
+ )
+ conn.execute(
+ "CREATE TABLE steps (id VARCHAR(64) PRIMARY KEY, workflow_id VARCHAR(64), "
+ "action_type VARCHAR(64))"
+ )
+ conn.execute(
+ "INSERT INTO workflows VALUES (?,?,?,?,?,?,?,?)",
+ ("wf_aiva", "Urgence_aiva_demo", "demo", "2026-06-01", "2026-06-18",
+ 1, "manual", ""),
+ )
+ conn.execute(
+ "INSERT INTO workflows VALUES (?,?,?,?,?,?,?,?)",
+ ("wf_learned", "Learned_flow", "", "2026-06-02", "2026-06-17",
+ 1, "learned_import", "pending"),
+ )
+ # 3 steps pour wf_aiva → nodes_count attendu = 3
+ for i in range(3):
+ conn.execute(
+ "INSERT INTO steps VALUES (?,?,?)", (f"s{i}", "wf_aiva", "click")
+ )
+ conn.commit()
+ conn.close()
+
+ monkeypatch.setattr(dashboard_app, "VWB_DB_PATH", Path(db_path))
+
+ resp = client.get('/api/workflows')
+ assert resp.status_code == 200
+ data = resp.get_json()
+ assert data['total'] == 2, f"attendu 2 workflows, obtenu {data['total']}"
+ names = {w['name'] for w in data['workflows']}
+ assert 'Urgence_aiva_demo' in names
+ aiva = next(w for w in data['workflows'] if w['name'] == 'Urgence_aiva_demo')
+ assert aiva['nodes_count'] == 3
+ assert aiva['source'] == 'manual'
+
def test_sessions_list(self, client):
"""L'API sessions retourne la liste."""
resp = client.get('/api/agent/sessions')
diff --git a/web_dashboard/app.py b/web_dashboard/app.py
index 7ee00c811..aec1edaf9 100644
--- a/web_dashboard/app.py
+++ b/web_dashboard/app.py
@@ -189,6 +189,20 @@ SESSIONS_PATH = DATA_PATH / "sessions"
WORKFLOWS_PATH = DATA_PATH / "workflows"
LOGS_PATH = BASE_PATH / "logs"
+# Source canonique des workflows (décision produit D3) : la base VWB v3
+# (SQLAlchemy/SQLite) que Léa lit déjà au runtime. Chemin absolu robuste (PAS la
+# DB fantôme vide à la racine du repo `instance/workflows.db`, schéma obsolète,
+# ni l'ancien store JSON `data/training/workflows/` créé vide sur DGX).
+# Surchargeable via RPA_VWB_DB_PATH pour les déploiements atypiques.
+def _resolve_vwb_db_path() -> Path:
+ override = os.getenv("RPA_VWB_DB_PATH", "").strip()
+ if override:
+ return Path(override).expanduser()
+ return BASE_PATH / "visual_workflow_builder" / "backend" / "instance" / "workflows.db"
+
+
+VWB_DB_PATH = _resolve_vwb_db_path()
+
# StorageManager
storage = StorageManager(base_path=str(DATA_PATH))
@@ -261,7 +275,9 @@ def system_status():
"""Statut du système."""
try:
sessions_count = len(list(SESSIONS_PATH.glob('*'))) if SESSIONS_PATH.exists() else 0
- workflows_count = len(list(WORKFLOWS_PATH.glob('*.json'))) if WORKFLOWS_PATH.exists() else 0
+ # Source canonique D3 : base VWB v3 (même comptage que /api/workflows),
+ # pas l'ancien store JSON `data/training/workflows/` créé vide sur DGX.
+ workflows_count = len(_load_workflows_from_vwb_db())
dependencies_ok = True
try:
@@ -785,36 +801,83 @@ def rename_session_workflow(session_id):
# API Workflows
# =============================================================================
+def _load_workflows_from_vwb_db() -> list:
+ """Charge les workflows depuis la base VWB v3 (source canonique D3).
+
+ Lit directement le SQLite que Léa interroge au runtime (cf.
+ `agent_chat/app.py` → `GET /api/v3/session/state`). On compte les `steps`
+ par workflow pour `nodes_count` (pas de notion d'`edges` en DAG linéaire :
+ `edges_count` = max(steps-1, 0)). Robuste à l'absence de la DB ou des
+ colonnes `source`/`review_status` (DB ancienne) : retourne [] sans planter.
+ """
+ import sqlite3
+
+ if not VWB_DB_PATH.exists():
+ return []
+
+ workflows = []
+ conn = sqlite3.connect(str(VWB_DB_PATH))
+ try:
+ conn.row_factory = sqlite3.Row
+ # Colonnes disponibles (la DB fantôme/ancienne n'a pas source/review_status)
+ cols = {row[1] for row in conn.execute("PRAGMA table_info(workflows)")}
+ has_source = 'source' in cols
+ has_review = 'review_status' in cols
+
+ select_cols = ['id', 'name', 'description', 'created_at', 'updated_at']
+ if has_source:
+ select_cols.append('source')
+ if has_review:
+ select_cols.append('review_status')
+
+ # Nombre de steps par workflow (= nodes du DAG)
+ step_counts = {
+ row[0]: row[1]
+ for row in conn.execute(
+ "SELECT workflow_id, COUNT(*) FROM steps GROUP BY workflow_id"
+ )
+ }
+
+ rows = conn.execute(
+ f"SELECT {', '.join(select_cols)} FROM workflows ORDER BY updated_at DESC"
+ ).fetchall()
+
+ for row in rows:
+ wf_id = row['id']
+ nodes_count = step_counts.get(wf_id, 0)
+ workflows.append({
+ 'workflow_id': wf_id,
+ 'name': row['name'] or wf_id,
+ 'description': row['description'] or '',
+ 'nodes_count': nodes_count,
+ 'edges_count': max(nodes_count - 1, 0),
+ 'learning_state': 'OBSERVATION',
+ 'created_at': str(row['created_at'] or ''),
+ 'updated_at': str(row['updated_at'] or ''),
+ 'execution_count': 0,
+ 'source': row['source'] if has_source else 'manual',
+ 'review_status': row['review_status'] if has_review else '',
+ 'file_path': f"vwb_db://{wf_id}",
+ })
+ finally:
+ conn.close()
+
+ return workflows
+
+
@app.route('/api/workflows')
def list_workflows():
- """Liste tous les workflows."""
+ """Liste tous les workflows depuis la base VWB v3 (source canonique D3).
+
+ Avant ce correctif, l'endpoint globait `data/training/workflows/*.json`
+ (ancien store JSON, créé vide sur DGX) et renvoyait toujours `total: 0`,
+ rendant la surface « ce que Léa sait » faussement vide. On lit désormais la
+ même base SQLite que Léa au runtime.
+ """
try:
- workflows = []
hide_unnamed = request.args.get('hide_unnamed', 'true').lower() == 'true'
- if not WORKFLOWS_PATH.exists():
- WORKFLOWS_PATH.mkdir(parents=True, exist_ok=True)
- return jsonify({'workflows': [], 'total': 0, 'hidden_unnamed': 0})
-
- for wf_file in WORKFLOWS_PATH.glob('*.json'):
- try:
- with open(wf_file, 'r') as f:
- wf_data = json.load(f)
-
- workflows.append({
- 'workflow_id': wf_data.get('workflow_id', wf_file.stem),
- 'name': wf_data.get('name', wf_file.stem),
- 'description': wf_data.get('description', ''),
- 'nodes_count': len(wf_data.get('nodes', [])),
- 'edges_count': len(wf_data.get('edges', [])),
- 'learning_state': wf_data.get('learning_state', 'OBSERVATION'),
- 'created_at': wf_data.get('created_at', ''),
- 'updated_at': wf_data.get('updated_at', ''),
- 'execution_count': wf_data.get('execution_count', 0),
- 'file_path': str(wf_file)
- })
- except Exception as e:
- print(f"Erreur lecture workflow {wf_file}: {e}")
+ workflows = _load_workflows_from_vwb_db()
# Filtrer les workflows "Unnamed" si demandé
if hide_unnamed:

View File

@@ -0,0 +1,310 @@
diff --git a/agent_v0/server_v1/api_stream.py b/agent_v0/server_v1/api_stream.py
index 547aeb299..aa620853b 100644
--- a/agent_v0/server_v1/api_stream.py
+++ b/agent_v0/server_v1/api_stream.py
@@ -835,15 +835,56 @@ def _get_worker_queue_status() -> Dict[str, Any]:
components_ready = bool(components) and all(bool(v) for v in components.values())
health_status = (health or {}).get("status")
running = bool(health) and not health_stale and health_status != "stopped"
+
+ # Distinction VEILLE (armé, lazy) vs DÉGRADÉ (vrai échec).
+ #
+ # Les composants lourds (ScreenAnalyzer/CLIP/FAISS/StateEmbedding) sont
+ # chargés en lazy par run_worker : le processor n'est instancié qu'au
+ # premier _process_session (cf. run_worker._get_processor / _process_session).
+ # Un worker neuf qui n'a jamais reçu de session écrit donc status="healthy"
+ # avec tous les composants à false — c'est l'état NORMAL « en veille », pas
+ # une panne. L'étiqueter "degraded" fait lire une panne là où il n'y en a pas.
+ #
+ # Signal retenu pour « init jamais tentée » : TOUS les composants à false ET
+ # sessions_processed == 0 ET sessions_failed == 0. Justification : run_worker
+ # n'appelle _get_processor() (donc l'init lazy) que dans _process_session, qui
+ # incrémente toujours exactement un compteur (processed / failed / skipped).
+ # Tant que processed == 0 ET failed == 0, aucune session n'a déclenché une
+ # init suivie d'un traitement — le worker est armé en attente. Un simple skip
+ # (dossier/shots absents) passe quand même par _get_processor() : les
+ # composants se chargent, donc tous-à-false devient faux et on n'entre pas ici.
+ # run_worker._health_components() écrit toujours les 4 clés (jamais un dict
+ # vide), d'où le test sur les VALEURS et non sur la présence des clés.
+ # Si run_worker a lui-même forcé status="degraded" (VLM + ScreenAnalyzer
+ # absent, cf. run_worker._write_health), c'est un VRAI échec : on le conserve.
+ stats = (health or {}).get("stats") or {}
+ init_attempted = bool(stats.get("sessions_processed", 0)) or bool(
+ stats.get("sessions_failed", 0)
+ )
+ components_all_false = bool(components) and not any(
+ bool(v) for v in components.values()
+ )
+ armed = (
+ running
+ and not components_ready
+ and health_status == "healthy"
+ and components_all_false # aucun composant lourd encore chargé
+ and not init_attempted
+ )
+
status = health_status or "unknown"
if not running:
status = "stale" if health else "unknown"
+ elif armed:
+ # En veille : worker sain, composants chargés à la 1re session.
+ status = "idle"
elif not components_ready:
status = "degraded"
return {
"running": running,
"status": status,
+ "armed": armed,
"queue_length": len(queue),
"queue": queue,
"replay_lock_active": REPLAY_LOCK_FILE.exists(),
@@ -858,11 +899,29 @@ def _get_worker_queue_status() -> Dict[str, Any]:
"components": components,
"components_ready": components_ready,
"processing_ready": running and not REPLAY_LOCK_FILE.exists() and components_ready,
- "stats": (health or {}).get("stats") or {},
+ "status_hint": _worker_status_hint(status, armed),
+ "stats": stats,
"note": "Le worker VLM tourne dans un process séparé (agent_v0.server_v1.run_worker).",
}
+def _worker_status_hint(status: str, armed: bool) -> str:
+ """Message humain pour le statut worker (consommé par le dashboard)."""
+ if armed or status == "idle":
+ return "En veille — composants chargés à la 1re session."
+ if status == "degraded":
+ return "Worker apprentissage dégradé — init des composants en échec."
+ if status == "stale":
+ return "Health file périmé (> 180s) — worker peut-être arrêté."
+ if status == "stopped":
+ return "Worker arrêté."
+ if status == "busy":
+ return "Traitement d'une session en cours."
+ if status == "healthy":
+ return "Worker prêt — composants chargés."
+ return "État worker inconnu."
+
+
# =========================================================================
# Compteur d'analyses en cours par session (pour attendre avant finalize)
# =========================================================================
diff --git a/tests/integration/test_stream_processor.py b/tests/integration/test_stream_processor.py
index 660187901..344e614cb 100644
--- a/tests/integration/test_stream_processor.py
+++ b/tests/integration/test_stream_processor.py
@@ -1289,3 +1289,158 @@ class TestAPIEndpoints:
assert len(workflows) == 1
assert workflows[0]["workflow_id"] == "wf_api_001"
assert workflows[0]["nodes"] == 2
+
+
+class TestWorkerStatusTruthfulness:
+ """Truthfulness du statut worker exposé par _get_worker_queue_status.
+
+ Distingue VEILLE (armé, lazy : worker neuf qui n'a jamais traité de
+ session, composants chargés à la 1re session) de DÉGRADÉ (init tentée
+ et en échec). Un worker en veille ne doit JAMAIS être étiqueté 'degraded'.
+ """
+
+ # Même contrainte que TestAPIEndpoints : api_stream fail-closed à l'import
+ # si RPA_API_TOKEN absent.
+ _TEST_API_TOKEN = "test_token_for_worker_status_0123456789abcdef"
+
+ @pytest.fixture(autouse=True)
+ def _ensure_api_token(self, monkeypatch):
+ monkeypatch.setenv("RPA_API_TOKEN", self._TEST_API_TOKEN)
+
+ @pytest.fixture
+ def status_env(self, tmp_path, monkeypatch):
+ """Isole les fichiers worker (health/queue/lock) sur tmp_path."""
+ from agent_v0.server_v1 import api_stream
+
+ health_file = tmp_path / "_worker_health.json"
+ queue_file = tmp_path / "_worker_queue.txt"
+ lock_file = tmp_path / "_replay_active.lock"
+ monkeypatch.setattr(api_stream, "WORKER_HEALTH_FILE", health_file)
+ monkeypatch.setattr(api_stream, "WORKER_QUEUE_FILE", queue_file)
+ monkeypatch.setattr(api_stream, "REPLAY_LOCK_FILE", lock_file)
+ return api_stream, health_file
+
+ @staticmethod
+ def _write_health(health_file, **overrides):
+ """Écrit un health file frais (mtime récent => non stale)."""
+ payload = {
+ "pid": 1234,
+ "started_at": "2026-06-18T10:00:00",
+ "last_cycle": "2026-06-18T10:00:30",
+ "current_session": None,
+ "queue_length": 0,
+ "components": {
+ "screen_analyzer": False,
+ "clip_embedder": False,
+ "faiss_manager": False,
+ "state_embedding_builder": False,
+ },
+ "stats": {
+ "sessions_processed": 0,
+ "sessions_failed": 0,
+ "sessions_skipped": 0,
+ "total_screenshots_analyzed": 0,
+ },
+ "status": "healthy",
+ }
+ payload.update(overrides)
+ health_file.write_text(json.dumps(payload), encoding="utf-8")
+
+ def test_fresh_worker_is_idle_not_degraded(self, status_env):
+ """Worker neuf : healthy, 0 session, tous composants false
+ => statut 'idle' (en veille / armé), PAS 'degraded'."""
+ api_stream, health_file = status_env
+ self._write_health(health_file) # défaut = état neuf
+
+ status = api_stream._get_worker_queue_status()
+
+ assert status["running"] is True
+ assert status["status"] == "idle", status
+ assert status["armed"] is True
+ assert status["components_ready"] is False
+ # processing_ready reste False tant que les composants ne sont pas chargés
+ assert status["processing_ready"] is False
+ assert "veille" in status["status_hint"].lower()
+
+ def test_worker_init_failed_is_degraded(self, status_env):
+ """Init tentée et en échec : run_worker force status='degraded'
+ (VLM + ScreenAnalyzer absent) => on conserve 'degraded'."""
+ api_stream, health_file = status_env
+ self._write_health(
+ health_file,
+ status="degraded", # forcé par run_worker._write_health
+ components={
+ "screen_analyzer": False,
+ "clip_embedder": True,
+ "faiss_manager": True,
+ "state_embedding_builder": False,
+ },
+ stats={
+ "sessions_processed": 0,
+ "sessions_failed": 1, # une session a tenté l'init et échoué
+ "sessions_skipped": 0,
+ "total_screenshots_analyzed": 0,
+ },
+ )
+
+ status = api_stream._get_worker_queue_status()
+
+ assert status["running"] is True
+ assert status["status"] == "degraded", status
+ assert status["armed"] is False
+ assert status["processing_ready"] is False
+ assert "dégradé" in status["status_hint"].lower()
+
+ def test_worker_partial_components_after_attempt_is_degraded(self, status_env):
+ """Composants partiels après tentative de traitement (sessions_failed>0),
+ sans status forcé par le worker => 'degraded' (pas 'idle')."""
+ api_stream, health_file = status_env
+ self._write_health(
+ health_file,
+ status="healthy",
+ components={
+ "screen_analyzer": True,
+ "clip_embedder": True,
+ "faiss_manager": False, # un composant manquant
+ "state_embedding_builder": True,
+ },
+ stats={
+ "sessions_processed": 0,
+ "sessions_failed": 2,
+ "sessions_skipped": 0,
+ "total_screenshots_analyzed": 0,
+ },
+ )
+
+ status = api_stream._get_worker_queue_status()
+
+ assert status["status"] == "degraded", status
+ assert status["armed"] is False
+
+ def test_worker_ready_after_processing_is_healthy(self, status_env):
+ """Worker ayant traité au moins une session, tous composants chargés
+ => 'healthy' et processing_ready=True."""
+ api_stream, health_file = status_env
+ self._write_health(
+ health_file,
+ status="healthy",
+ components={
+ "screen_analyzer": True,
+ "clip_embedder": True,
+ "faiss_manager": True,
+ "state_embedding_builder": True,
+ },
+ stats={
+ "sessions_processed": 3,
+ "sessions_failed": 0,
+ "sessions_skipped": 0,
+ "total_screenshots_analyzed": 42,
+ },
+ )
+
+ status = api_stream._get_worker_queue_status()
+
+ assert status["status"] == "healthy", status
+ assert status["armed"] is False
+ assert status["components_ready"] is True
+ assert status["processing_ready"] is True
diff --git a/web_dashboard/templates/index.html b/web_dashboard/templates/index.html
index c96cc8bf4..aeb0e7fa8 100644
--- a/web_dashboard/templates/index.html
+++ b/web_dashboard/templates/index.html
@@ -2838,13 +2838,23 @@
]);
const processingReady = processing && processing.processing_ready === true;
- const processingDegraded = processing && !processing.error && !processingReady;
+ // « En veille » (armé/lazy) ≠ « dégradé » : un worker neuf sans
+ // session a tous ses composants à false par design (chargement à la
+ // 1re session), ce n'est PAS une panne. Seul status==='degraded'
+ // (init tentée et en échec) est une vraie alerte.
+ const processingArmed = processing && (processing.armed === true || processing.status === 'idle');
+ const processingDegraded = processing && !processing.error && processing.status === 'degraded';
+ const statusHint = (processing && processing.status_hint) || '';
statusEl.innerHTML = processingDegraded
? '<span style="color:#f59e0b;">⚠️</span>'
- : '<span style="color:#22c55e;">✅</span>';
+ : processingArmed
+ ? '<span style="color:#3b82f6;">⏸️</span>'
+ : '<span style="color:#22c55e;">✅</span>';
statusEl.title = processingDegraded
- ? 'Streaming en ligne, worker apprentissage dégradé'
- : 'Serveur streaming en ligne';
+ ? `Streaming en ligne, worker apprentissage dégradé${statusHint ? ' — ' + statusHint : ''}`
+ : processingArmed
+ ? `Streaming en ligne, worker en veille${statusHint ? ' — ' + statusHint : ''}`
+ : 'Serveur streaming en ligne';
document.getElementById('streamActiveSessions').textContent = data.active_sessions || 0;
document.getElementById('streamTotalEvents').textContent = data.total_events || 0;
@@ -2862,14 +2872,20 @@
if (data.server_version) rows.push({label: 'Version serveur', value: data.server_version});
if (processing && !processing.error) {
const status = processing.status || 'unknown';
+ const workerIcon = processingReady ? '✅' : (processingArmed ? '⏸️' : '⚠️');
rows.push({
label: 'Worker apprentissage',
- value: processingReady ? `✅ ${status}` : `⚠️ ${status}`
+ value: `${workerIcon} ${status}`
});
rows.push({
label: 'Composants intelligence',
- value: processing.components_ready ? 'prêts' : 'non prêts'
+ value: processing.components_ready
+ ? 'prêts'
+ : (processingArmed ? 'en veille (chargés à la 1re session)' : 'non prêts')
});
+ if (statusHint) {
+ rows.push({label: 'Détail worker', value: statusHint});
+ }
if (processing.queue_length !== undefined) {
rows.push({label: 'Queue apprentissage', value: processing.queue_length});
}

View File

@@ -0,0 +1,318 @@
diff --git a/visual_workflow_builder/backend/app.py b/visual_workflow_builder/backend/app.py
index 7bdae57b0..c3a285cc0 100644
--- a/visual_workflow_builder/backend/app.py
+++ b/visual_workflow_builder/backend/app.py
@@ -28,6 +28,109 @@ load_dotenv() # fallback .env dans cwd (n'écrase pas les vars déjà définies
# Initialize Flask app
app = Flask(__name__)
+# ============================================================
+# HTTP Basic Auth LAN (cohérent avec le dashboard 5001)
+# ============================================================
+# Le VWB (backend 5002) était exposé au LAN SANS authentification. On ajoute
+# un middleware before_request qui exige un header Authorization: Basic <b64>
+# pour toute requête NON-loopback (LAN), avec les MÊMES credentials que le
+# dashboard : DASHBOARD_USER / DASHBOARD_PASSWORD (dans .env.local).
+#
+# GARDE-FOU CRITIQUE — exemption loopback :
+# Le dashboard (agent_chat/app.py `_fetch_vwb_workflows`) et les healthchecks
+# appellent ce backend en boucle locale (http://localhost:5002 → 127.0.0.1).
+# Exiger l'auth en loopback CASSERAIT l'intégration dashboard↔VWB. On exempte
+# donc 127.0.0.1 / ::1 (et ::ffff:127.0.0.1) de toute auth.
+#
+# Différence assumée avec le dashboard (fail-closed) : ici on NE crashe PAS si
+# DASHBOARD_PASSWORD est absent. On log un warning et on laisse passer le LAN
+# (mode POC dev/dégradé). En clinique, DASHBOARD_PASSWORD est défini dans
+# .env.local (chargé ci-dessus, lignes 24-26) → l'auth LAN est effective.
+import base64 as _base64
+import hmac as _hmac
+
+_VWB_AUTH_USER = os.getenv("DASHBOARD_USER", "lea").strip()
+_VWB_AUTH_PASSWORD = os.getenv("DASHBOARD_PASSWORD", "").strip()
+# Désactivation explicite (dev/tests, parité avec le dashboard).
+_VWB_AUTH_DISABLED = os.getenv("DASHBOARD_AUTH_DISABLED", "").lower() in (
+ "1", "true", "yes",
+)
+
+# Adresses considérées comme loopback (server-to-server, jamais challengées).
+_VWB_LOOPBACK_ADDRS = {"127.0.0.1", "::1", "::ffff:127.0.0.1"}
+
+# Paths publics (pas d'auth) — healthchecks systemd / NPM / smokes.
+_VWB_PUBLIC_PATHS = {"/health", "/api/health"}
+
+if not _VWB_AUTH_PASSWORD and not _VWB_AUTH_DISABLED:
+ logging.getLogger("vwb.auth").warning(
+ "[SECURITE] DASHBOARD_PASSWORD non defini : l'auth Basic LAN du VWB "
+ "(5002) est INACTIVE (le LAN passe sans credentials). Definir "
+ "DASHBOARD_PASSWORD dans .env.local pour l'activer (cible clinique)."
+ )
+
+
+def _vwb_auth_ok(header_value: str) -> bool:
+ """Valide le header Authorization Basic. Comparaison constant-time.
+
+ Logique identique au dashboard (`web_dashboard/app.py::_dashboard_auth_ok`).
+ """
+ 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(":")
+ user_ok = _hmac.compare_digest(user, _VWB_AUTH_USER)
+ pwd_ok = _hmac.compare_digest(password, _VWB_AUTH_PASSWORD)
+ return user_ok and pwd_ok
+
+
+@app.before_request
+def _vwb_basic_auth_middleware():
+ """Middleware d'auth HTTP Basic LAN sur le backend VWB (port 5002).
+
+ - Bypass total si DASHBOARD_AUTH_DISABLED=true (dev/tests).
+ - Bypass total si DASHBOARD_PASSWORD absent (mode POC degrade, warning emis
+ au demarrage) — on ne casse pas le service faute de secret.
+ - Loopback (127.0.0.1 / ::1) : JAMAIS challenge (proxy dashboard, healthcheck).
+ - Preflight CORS (OPTIONS) : laisse passer (le navigateur n'envoie pas
+ l'en-tete Authorization au preflight).
+ - Paths publics (_VWB_PUBLIC_PATHS) : healthchecks externes.
+ - Sinon (requete LAN) : header Authorization: Basic <b64> obligatoire, sinon 401.
+ """
+ from flask import request, Response
+
+ # Dev / tests / mode degrade sans secret : bypass total
+ if _VWB_AUTH_DISABLED or not _VWB_AUTH_PASSWORD:
+ return None
+
+ # Preflight CORS : pas d'auth (le navigateur n'envoie pas les credentials)
+ if request.method == "OPTIONS":
+ return None
+
+ # Exemption loopback (server-to-server : dashboard, healthcheck)
+ if (request.remote_addr or "") in _VWB_LOOPBACK_ADDRS:
+ return None
+
+ # Paths publics (healthchecks externes)
+ if (request.path or "/") in _VWB_PUBLIC_PATHS:
+ return None
+
+ if _vwb_auth_ok(request.headers.get("Authorization", "")):
+ return None
+
+ # Pas authentifie — challenge 401 avec WWW-Authenticate
+ return Response(
+ '{"error": "authentication required"}',
+ status=401,
+ mimetype="application/json",
+ headers={"WWW-Authenticate": 'Basic realm="RPA Vision V3 VWB"'},
+ )
+
# ============================================================
# Logging — fichier rotatif + console (idempotent)
# ============================================================
diff --git a/visual_workflow_builder/backend/instance/workflows.db b/visual_workflow_builder/backend/instance/workflows.db
index db6eabd62..b7e181cbe 100644
Binary files a/visual_workflow_builder/backend/instance/workflows.db and b/visual_workflow_builder/backend/instance/workflows.db differ
diff --git a/visual_workflow_builder/backend/tests/test_vwb_basic_auth.py b/visual_workflow_builder/backend/tests/test_vwb_basic_auth.py
new file mode 100644
index 000000000..f4bff4d9d
--- /dev/null
+++ b/visual_workflow_builder/backend/tests/test_vwb_basic_auth.py
@@ -0,0 +1,195 @@
+"""
+Tests de l'auth HTTP Basic LAN du backend VWB (port 5002).
+
+Le VWB etait expose au LAN SANS authentification. Le middleware
+`_vwb_basic_auth_middleware` ajoute un challenge 401 sur toute requete
+NON-loopback, avec les MEMES credentials que le dashboard
+(DASHBOARD_USER / DASHBOARD_PASSWORD).
+
+Controles cles :
+- Loopback (127.0.0.1) sans credentials -> 200 (proxy dashboard / healthcheck).
+- LAN (REMOTE_ADDR non loopback) sans credentials -> 401 + WWW-Authenticate.
+- LAN avec mauvais mot de passe -> 401.
+- LAN avec bons credentials -> passage (pas de 401).
+- /health public meme en LAN.
+- DASHBOARD_AUTH_DISABLED=true -> bypass total.
+- DASHBOARD_PASSWORD absent -> auth inactive (mode POC degrade, pas de crash).
+"""
+from __future__ import annotations
+
+import base64
+import importlib
+import os
+import sys
+from pathlib import Path
+
+import pytest
+
+# Le backend VWB s'importe en tant que module top-level `app`
+# (cf. tests/conftest.py : `from app import app, db`). On ajoute le repertoire
+# backend au path pour pouvoir le recharger avec les variables d'env voulues.
+_BACKEND_DIR = Path(__file__).resolve().parent.parent
+if str(_BACKEND_DIR) not in sys.path:
+ sys.path.insert(0, str(_BACKEND_DIR))
+
+# Adresse LAN simulee (non loopback)
+_LAN_ADDR = "192.168.1.50"
+_LAN_ENV = {"REMOTE_ADDR": _LAN_ADDR}
+
+
+def _basic_auth_header(user: str, password: str) -> str:
+ token = base64.b64encode(f"{user}:{password}".encode()).decode()
+ return f"Basic {token}"
+
+
+def _reload_app():
+ """Recharge le module `app` pour relire les constantes d'auth depuis l'env."""
+ if "app" in sys.modules:
+ return importlib.reload(sys.modules["app"])
+ return importlib.import_module("app")
+
+
+@pytest.fixture
+def auth_enabled_client(monkeypatch):
+ """Client VWB avec auth LAN active (DASHBOARD_USER/PASSWORD definis)."""
+ monkeypatch.setenv("DASHBOARD_USER", "lea")
+ monkeypatch.setenv("DASHBOARD_PASSWORD", "secret-test-pwd")
+ monkeypatch.delenv("DASHBOARD_AUTH_DISABLED", raising=False)
+ mod = _reload_app()
+ mod.app.config["TESTING"] = True
+ mod.app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
+ with mod.app.test_client() as c:
+ with mod.app.app_context():
+ mod.db.create_all()
+ yield c
+ mod.db.drop_all()
+
+
+@pytest.fixture
+def auth_disabled_client(monkeypatch):
+ """Client VWB avec auth desactivee (DASHBOARD_AUTH_DISABLED=true)."""
+ monkeypatch.setenv("DASHBOARD_AUTH_DISABLED", "true")
+ monkeypatch.setenv("DASHBOARD_PASSWORD", "secret-test-pwd")
+ mod = _reload_app()
+ mod.app.config["TESTING"] = True
+ mod.app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
+ with mod.app.test_client() as c:
+ with mod.app.app_context():
+ mod.db.create_all()
+ yield c
+ mod.db.drop_all()
+
+
+@pytest.fixture
+def no_password_client(monkeypatch):
+ """Client VWB sans DASHBOARD_PASSWORD (mode POC degrade : auth inactive)."""
+ monkeypatch.delenv("DASHBOARD_PASSWORD", raising=False)
+ monkeypatch.delenv("DASHBOARD_AUTH_DISABLED", raising=False)
+ mod = _reload_app()
+ mod.app.config["TESTING"] = True
+ mod.app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
+ with mod.app.test_client() as c:
+ with mod.app.app_context():
+ mod.db.create_all()
+ yield c
+ mod.db.drop_all()
+
+
+class TestVwbBasicAuth:
+ """Auth HTTP Basic LAN sur le backend VWB (5002)."""
+
+ def test_loopback_no_creds_passes(self, auth_enabled_client):
+ """Requete loopback (127.0.0.1) sans creds -> PAS de 401.
+
+ Garde-fou critique : le dashboard proxifie en loopback. La requete
+ ne doit jamais etre challengee (200, ou autre code applicatif != 401).
+ """
+ resp = auth_enabled_client.get("/api/v3/session/state")
+ assert resp.status_code != 401, (
+ f"Loopback ne doit jamais etre challenge (got {resp.status_code})"
+ )
+
+ def test_lan_no_creds_returns_401(self, auth_enabled_client):
+ """Requete LAN (non loopback) sans creds -> 401 + WWW-Authenticate."""
+ resp = auth_enabled_client.get(
+ "/api/v3/session/state", environ_base=_LAN_ENV
+ )
+ assert resp.status_code == 401
+ assert "WWW-Authenticate" in resp.headers
+ assert "Basic" in resp.headers["WWW-Authenticate"]
+
+ def test_lan_wrong_password_returns_401(self, auth_enabled_client):
+ """Requete LAN avec mauvais mot de passe -> 401."""
+ resp = auth_enabled_client.get(
+ "/api/v3/session/state",
+ environ_base=_LAN_ENV,
+ headers={"Authorization": _basic_auth_header("lea", "wrong")},
+ )
+ assert resp.status_code == 401
+
+ def test_lan_wrong_user_returns_401(self, auth_enabled_client):
+ """Requete LAN avec mauvais utilisateur -> 401."""
+ resp = auth_enabled_client.get(
+ "/api/v3/session/state",
+ environ_base=_LAN_ENV,
+ headers={"Authorization": _basic_auth_header("intruder", "secret-test-pwd")},
+ )
+ assert resp.status_code == 401
+
+ def test_lan_valid_credentials_pass(self, auth_enabled_client):
+ """Requete LAN avec bons creds -> PAS de 401 (auth franchie)."""
+ resp = auth_enabled_client.get(
+ "/api/v3/session/state",
+ environ_base=_LAN_ENV,
+ headers={"Authorization": _basic_auth_header("lea", "secret-test-pwd")},
+ )
+ assert resp.status_code != 401, (
+ f"Bons creds doivent franchir l'auth (got {resp.status_code})"
+ )
+
+ def test_lan_malformed_header_returns_401(self, auth_enabled_client):
+ """Requete LAN avec header mal forme (Bearer) -> 401."""
+ resp = auth_enabled_client.get(
+ "/api/v3/session/state",
+ environ_base=_LAN_ENV,
+ headers={"Authorization": "Bearer tototoken"},
+ )
+ assert resp.status_code == 401
+
+ def test_lan_health_is_public(self, auth_enabled_client):
+ """/health reste public meme en LAN (healthcheck externe)."""
+ resp = auth_enabled_client.get("/health", environ_base=_LAN_ENV)
+ assert resp.status_code == 200
+
+ def test_lan_options_preflight_not_blocked(self, auth_enabled_client):
+ """Preflight CORS (OPTIONS) en LAN -> pas de 401 (CORS preserve)."""
+ resp = auth_enabled_client.open(
+ "/api/v3/session/state", method="OPTIONS", environ_base=_LAN_ENV
+ )
+ assert resp.status_code != 401
+
+ def test_auth_disabled_bypass_lan(self, auth_disabled_client):
+ """DASHBOARD_AUTH_DISABLED=true -> LAN passe sans creds."""
+ resp = auth_disabled_client.get(
+ "/api/v3/session/state", environ_base=_LAN_ENV
+ )
+ assert resp.status_code != 401
+
+ def test_no_password_degraded_lan_passes(self, no_password_client):
+ """DASHBOARD_PASSWORD absent -> mode POC degrade : LAN passe (pas de crash)."""
+ resp = no_password_client.get(
+ "/api/v3/session/state", environ_base=_LAN_ENV
+ )
+ assert resp.status_code != 401
+
+
+@pytest.fixture(autouse=True)
+def _restore_module(monkeypatch):
+ """Restaure le module `app` en mode auth desactivee apres chaque test,
+ pour ne pas contaminer les autres tests VWB (qui importent `app`)."""
+ yield
+ monkeypatch.setenv("DASHBOARD_AUTH_DISABLED", "true")
+ monkeypatch.delenv("DASHBOARD_PASSWORD", raising=False)
+ monkeypatch.delenv("DASHBOARD_USER", raising=False)
+ if "app" in sys.modules:
+ importlib.reload(sys.modules["app"])

View File

@@ -0,0 +1,83 @@
# Registre des décisions — 2026-06-08
- `Date`: 2026-06-08
- `Auteur`: Qwen (compilation)
- `Statut`: actif, source de vérité jusqu'à contre-ordre Dom
---
## Décisions tranchées
### D-01 : DGX = Option A (court terme)
- **Cible** : `/home/aivanov/ai/rpa_vision_v3`, user `aivanov`
- **Tranché par** : Dom (15:15)
- **Référence** : `active/2026-06-08_1515_decisions-dom-go-operationnels.md`
- **Branch** : `poc-dgx` poussée sur Gitea (`6d34b3cb6`)
- **Option B** (`/opt/rpa_vision_v3`, user `rpa`) : reportée post-POC
### D-02 : WP-A dashboard fail-closed — GO
- **Ce que ça fait** : Dashboard refuse de démarrer si `DASHBOARD_PASSWORD` absent ET auth non désactivée. Mot de passe par défaut supprimé.
- **Commit** : `549ea0631`
- **Tests** : 11/11 passés
- **Tranché par** : Dom (15:15) + QG Qwen (15:48)
### D-03 : WP-B verrou enrôlement — GO
- **Ce que ça fait** : `RPA_FLEET_ENROLL_LOCKED=true` bloque enrôlement nouveau `machine_id`. Ferme contournement "poste révoqué + nouveau ID + token global".
- **Commit** : `f18de016d`
- **Tests** : 6/6 passés
- **Tranché par** : Dom (15:15) + QG Qwen (15:48)
### D-04 : P1.g GPU merge — GO
- **Ce que ça fait** : `resolve_device(auto/cuda/cpu)` avec garde-fou VRAM (`min_free_gb=2`, `max_total_gb=6`), fallback CPU.
- **Commit** : `0e215da84`
- **Tests** : 15/15 passés, smoke OK (`auto → cuda`, `cpu → cpu`)
- **Tranché par** : Dom (15:15) + QG Qwen (15:25)
- **Rollback** : `RPA_VISION_DEVICE=cpu`, `RPA_EASYOCR_GPU=0`
### D-05 : Grounder candidat = Qwen3-VL-4B-Instruct via vLLM
- **Bench** : 87.5% accuracy, 1.1s latence, 1 dangereux/16, Apache-2.0
- **Tranché par** : Claude (15:45) + QG Qwen (16:10)
- **Note** : Pas d'activation runtime sans GO Dom + bench sur écrans réels
### D-06 : Grounding gemma4:26b supervisé
- **Bench** : 69% accuracy, **0 dangereux**, 14.4s OCR
- **Tranché par** : Claude (15:45) + QG Qwen (16:10)
- **Usage** : Grounding supervisé (pas autonome)
### D-07 : UI-TARS gate vision
- **Ce que ça fait** : `has_vision_capability()` via `/api/show` — skip propre si modèle aveugle + `logger.warning`
- **Commit** : `d00fe7b00`
- **Tranché par** : Claude (12:25) + QG Qwen (12:28)
- **Contexte** : UI-TARS cassé sur DGX (aveugle, mmproj non chargé) → 500 silencieux corrigé
### D-08 : Trained artifacts V2 — GO avec réserves
- **Paquet** : ~306 Mo, 6107 fichiers (anchors ajoutés, screen_states retirés)
- **Anti-secret** : CLEAN (0 token, 0 identité patient, 0 machine_id réel)
- **Rewrite** : 2 colonnes (`image_path` + `thumbnail_path`) `/home/dom/``/home/aivanov/`
- **Revue visuelle** : Dom confirme captures 100% fictives (mockup `urgence.labs`)
- **Tranché par** : Claude (16:08) + Codex (16:06) + QG Qwen (16:10, 16:50)
- **Transfert** : Bloqué en attente GO Dom effectif
### D-09 : Lea live — GO quand preflight vert
- **Condition** : Dom devant Windows + preflight G1-G6 verts
- **Scope** : Capture Shadow supervisée, Notepad/Explorateur/Easily lecture seule
- **Interdit** : ❌ Replay autonome
- **Tranché par** : Dom (15:15)
### D-10 : Code mort — NO-GO suppression
- **Ce qui est interdit** : Suppression, déplacement, archive, marquage `DEPRECATED` massif
- **Autorisé** : Inventaire read-only, documentation
- **Tranché par** : Dom (16:37) + Codex (16:37) + Claude (16:35)
- **Référence** : `active/2026-06-08_1637_decision-piste2-no-go-code-mort.md`
---
## Décisions en attente
| Sujet | En attente de | Impact |
|---|---|---|
| Unification `.service` files | Conception Codex | Migration DGX propre |
| Transfert trained artifacts DGX | GO Dom effectif + tar créé | Clone DGX complet |
| Node.js sur DGX | Dom/Claude | VWB frontend 3002 |
| Benchmark GPU P1.g | Claude (en cours) | Activation large P1.g |
| Scan anti-code-mort | Attente (read-only) | Détection automatique "UI-TARS bis" |

View File

@@ -0,0 +1,145 @@
# Registre des decisions — 2026-06-09
- `Date`: 2026-06-09
- `Auteur`: Codex
- `Statut`: actif
---
## Decisions tranchees
### D-2026-06-09-01 : Codex orchestre activement Claude et Qwen
- **Tranche par** : Dom, 2026-06-09
- **Decision** : Codex est l'orchestrateur actif du projet. Tant que Claude et Qwen ont des loops de coordination actifs, Codex doit leur fournir une prochaine tache actionnable.
- **Exception** : Codex peut ne pas distribuer une execution seulement s'il attend explicitement un feu vert de Dom ou un QG bloquant.
- **Implication** : en cas de blocage sur execution, Codex distribue quand meme des taches non destructives : preparation, audit read-only, QG, rollback, plan de tests, registre decisions.
- **Persistance** : regle ajoutee a `docs/coordination/ROLES.md`.
### D-2026-06-09-02 : Dashboard DGX — GO partiel, produit incomplet
- **Tranche par** : QG Qwen + Codex
- **Refs** :
- `docs/coordination/inbox_codex/2026-06-09_1120_claude-to-codex_RESULTAT-DASHBOARD-DGX-E2E.md`
- `docs/coordination/inbox_codex/2026-06-09_1130_qwen-to-codex-claude-dom_QG-DASHBOARD-DGX-E2E-GO-PARTIEL.md`
- **Decision** : securite dashboard DGX validee ; dashboard produit non valide tant que P0 workflows non servis et ZIP agent absent ne sont pas corriges.
- **Suite autorisee** : preparation Lea live read-only ; preparation corrections P0 sans execution.
- **Bloquant multi-TIM** : WP-C token par poste.
### D-2026-06-09-03 : Branching stable = `poc-prod`
- **Tranche par** : Dom, relaye par Claude a 14:12 CEST.
- **Ref** : `docs/coordination/inbox_codex/2026-06-09_1412_claude-to-codex-qwen_DECISIONS-DOM-poc-prod-m4-clone-p0-go.md`
- **Decision** : branche stable POC/NVP/prod = `poc-prod`.
- **Schema retenu** : `main` racine, `dev` integration quotidienne, `poc-prod` stable protegee promue depuis `dev` apres QG.
- **Conservation** : `poc-dgx` conserve comme snapshot DGX.
- **Reste ouvert** : chantier blobs ~15 G, sort de `master`, droits de push/protection sur `poc-prod`.
### D-2026-06-09-04 : Docker — rester clone + venv + systemd pour le POC
- **Tranche par** : Dom, relaye par Claude a 14:12 CEST.
- **Refs** :
- `docs/coordination/inbox_codex/2026-06-09_1410_claude-to-codex_AUDIT-DOCKER-VS-CLONE-SYSTEMD.md`
- `docs/coordination/inbox_codex/2026-06-09_1412_claude-to-codex-qwen_DECISIONS-DOM-poc-prod-m4-clone-p0-go.md`
- **Decision court terme** : ne pas dockeriser pour le POC ; conserver clone + venv + systemd.
- **Decision moyen terme** : Docker hybride progressif post-POC, avec Ollama et agent RPA hors conteneur.
### D-2026-06-09-05 : GO execution P0-1/P0-2 dashboard DGX
- **Tranche par** : Dom, relaye par Claude a 14:12 CEST.
- **Ref** : `docs/coordination/inbox_codex/2026-06-09_1412_claude-to-codex-qwen_DECISIONS-DOM-poc-prod-m4-clone-p0-go.md`
- **P0-1** : appliquer `DATABASE_URL` absolu pour VWB, restart service, verifier `/api/workflows/` = 23.
- **P0-2** : build `deploy/Lea_v1.0.0.zip` sur DGX pour usage POC interne.
- **Garde-fou** : paquet agent non distribuable multi-TIM tant que WP-C token par poste n'est pas implemente et QG.
### D-2026-06-09-06 : M2 cible dev Linux, Ollama via tunnel DGX
- **Tranche par** : QG Qwen corrige a 15:12 CEST.
- **Refs** :
- `docs/coordination/inbox_codex/2026-06-09_1420_claude-to-codex_RESULTAT-M2-VERIF-SERVEUR-DEV-LINUX-READONLY.md`
- `docs/coordination/inbox_codex/2026-06-09_1425_claude-to-codex_RESULTAT-M2-OLLAMA-ENV-READONLY.md`
- `docs/coordination/inbox_codex/2026-06-09_1512_qwen-to-codex-claude-dom_QG-M2-CORRECTION-OLLAMA-TUNNEL-GO.md`
- **Decision** : M2 cible le serveur dev Linux `192.168.1.40`, pas le DGX directement.
- **Ollama** : acces via tunnel SSH local `127.0.0.1:11434` vers DGX ; Qwen indique tunnel relance et modeles disponibles.
- **Garde-fou** : execution live Shadow uniquement avec Dom devant Windows + GO Codex. Tunnel non persistant = P1 a stabiliser avant demo multi-machine.
### D-2026-06-09-07 : WP-C peut commencer apres P0 dashboard
- **Tranche par** : QG Qwen a 15:05 et 15:06 CEST.
- **Refs** :
- `docs/coordination/inbox_codex/2026-06-09_1505_qwen-to-codex-claude-dom_QG-PREP-P0-DASHBOARD-WPC-GO.md`
- `docs/coordination/inbox_codex/2026-06-09_1506_qwen-to-codex-claude-dom_QG-POST-P0-BRANCHES-DOCKER.md`
- **Decision QG** : WP-C token par poste peut commencer en parallele M2 apres P0 dashboard.
- **Garde-fou** : WP-C reste bloquant multi-TIM ; tests P0 requis : build ZIP sans token global + enroll poste unique.
### D-2026-06-09-08 : P0 dashboard corrige par symlink workflow store
- **Tranche par** : Dom, relaye et execute par Claude a 20:25 CEST.
- **Ref** : `docs/coordination/inbox_codex/2026-06-09_2025_claude-to-codex-qwen_RESULTAT-P0-DASHBOARD-CORRECTIONS.md`
- **Rectificatif** : le diagnostic `DATABASE_URL` etait incomplet/errone ; la route VWB `/api/workflows/` lit les JSON via `WorkflowDatabase("data/workflows")`, pas SQLAlchemy.
- **Cause racine** : divergence de `WorkingDirectory` dev vs DGX, donc chemin relatif `data/workflows` resolu au mauvais endroit sur DGX.
- **Correction POC** : symlink `data/workflows` -> `visual_workflow_builder/backend/data/workflows`.
- **Verification** : VWB 5002 `/api/workflows/` = 39 workflows.
- **Rollback** : `rm data/workflows && mkdir data/workflows`.
- **Note** : `DATABASE_URL` laisse en place, neutre pour cette route, utile aux composants SQLAlchemy.
### D-2026-06-09-09 : DETTE-015 double store workflows
- **Ref** : `docs/coordination/inbox_codex/2026-06-09_2035_claude-to-codex-qwen_INFO-DETTE-015-store-workflows.md`
- **Decision** : enregistrer la dette double store workflows en P2 post-POC.
- **Docs** :
- `docs/DETTE_TECHNIQUE.md`
- `docs/PLAN_MIGRATION_WORKFLOWS_STORE_2026-06-09.md`
- **Cible post-POC** : unifier sur la DB SQLAlchemy existante sous flag, TDD, sans refactor avant la demo.
### D-2026-06-09-10 : Qwen indisponible, Codex orchestre en mode degrade
- **Tranche par** : Dom, 2026-06-09.
- **Decision** : Qwen est considere temporairement indisponible. Codex ne lui route plus de nouvelles taches tant que la panne n'est pas levee.
- **Implication** : Codex assure l'interim de coordination et demande a Claude des self-checks/read-only/handoffs, sans executer les actions qui exigent un QG formel si elles sont structurelles ou risquées.
- **Garde-fou** : pas de code WP-C, pas de live M2, pas de multi-TIM sans GO explicite Dom/Codex et, si possible, QG a posteriori quand Qwen revient.
### D-2026-06-09-11 : Qwen revenu, reprise QG normale
- **Tranche par** : Dom/Codex, 2026-06-09 20:47 CEST.
- **Refs** :
- `docs/coordination/inbox_codex/2026-06-09_2025_qwen-to-codex-claude-dom_QG-TUNNEL-PERSISTANT-M2-READINESS-GO.md`
- `docs/coordination/inbox_codex/2026-06-09_2026_qwen-to-codex-claude-dom_QG-WPC-CARTO-GO.md`
- `docs/coordination/inbox_codex/2026-06-09_2027_qwen-to-codex-claude-dom_ACK-MESSAGES-FIN-JOURNEE.md`
- **Decision** : Qwen est revenu ; Codex reprend le mode normal avec QG Qwen.
- **Action** : Qwen doit rendre le QG post-P0 dashboard sur le resultat Claude 20:25 ; Claude peut avancer WP-C Patch 1 local/TDD uniquement.
### D-2026-06-09-12 : Dashboard post-P0 — GO final
- **Tranche par** : QG Qwen a 20:50 CEST.
- **Refs** :
- `docs/coordination/inbox_codex/2026-06-09_2050_qwen-to-codex-claude-dom_QG-POST-P0-DASHBOARD-GO.md`
- `docs/coordination/inbox_codex/2026-06-09_2050_qwen-to-codex-claude-dom_ACK-POST-P0.md`
- **Decision** : P0-1 et P0-2 resolus ; dashboard produit debloque pour M2.
- **Verification Qwen** : VWB 5002 `/api/workflows/` = 39, symlink OK, tunnel Ollama actif.
- **Garde-fou** : ZIP agent POC interne uniquement ; multi-TIM reste NOGO avant WP-C.
- **Point P1** : verifier et retirer `DATABASE_URL` si present dans env systemd/service pour eviter dette inutile avant GO M2.
### D-2026-06-09-13 : WP-C Patch 1 GO, Patch 2 autorise local/TDD
- **Tranche par** : QG Qwen a 21:10 CEST + Codex.
- **Refs** :
- `docs/coordination/inbox_codex/2026-06-09_2058_claude-to-codex_RESULTAT-WPC-PATCH1-MIGRATION-TDD.md`
- `docs/coordination/inbox_codex/2026-06-09_2104_claude-to-codex_INFO-WPC-PATCH1-COMMITE.md`
- `docs/coordination/inbox_codex/2026-06-09_2110_qwen-to-codex-claude-dom_QG-WPC-PATCH1-GO.md`
- **Patch 1** : GO, commit local `f7f692641`, non pousse, non deploye DGX.
- **DATABASE_URL** : garder ; P1 post-POC, pas de cleanup avant M2.
- **Patch 2 autorise** : generation token a l'enroll, local/TDD uniquement, pas de branchement auth, pas de ZIP, pas de deploiement DGX.
---
## Decisions en attente
| Sujet | En attente de | Impact |
|---|---|---|
| WP-C Patch 2 local/TDD | Resultat Claude + QG Qwen | Generation token enroll seulement |
| Branching details : blobs/master/protection | QG Qwen + GO Dom | Nettoyage et protection stable |
| Execution M2 Lea live | Dom devant Windows + GO Codex | Capture Shadow supervisee |
| Push/deploiement WP-C Patch 1+ | GO Dom/Codex separe | Integration distante |
— Codex

View File

@@ -0,0 +1,62 @@
# Registre des decisions — 2026-06-10
- `Date`: 2026-06-10
- `Auteur`: Codex
- `Statut`: actif
---
## Decisions tranchees
### D-2026-06-10-01 : Plan de continuite si contexte Codex bas/reset
- **Tranche par** : Dom, relaye par Codex a 09:18 CEST.
- **Contexte** : Dom signale qu'il reste tres peu de contexte Codex et qu'un reset est prevu vers 14:12.
- **Decision** : Codex doit poser un plan de relais pour permettre a Claude et Qwen d'avancer sans dependance forte a son contexte.
- **Claude** : peut avancer WP-C Patch 2 local/TDD seulement et produire preuves/resultat.
- **Qwen** : garde le fil, rend QG, signale contradictions, tient active/registre si necessaire.
- **Dom** : reste arbitre pour live, push, deploiement, branches et extension de scope.
- **Refs** :
- `docs/handoffs/2026-06-10_handoff_codex_context-low-reset-1412.md`
- `docs/coordination/active/2026-06-10_0918_continuite-codex-context-reset.md`
- `docs/coordination/inbox_claude/2026-06-10_0918_codex-to-claude_RELANCE-WPC-PATCH2-CONTINUITE.md`
- `docs/coordination/inbox_qwen/2026-06-10_0918_codex-to-qwen_QG-CONTINUITE-CONTEXT-RESET.md`
### D-2026-06-10-02 : WP-C Patch 1-3 GO, arret avant Patch 4 runtime
- **Tranche par** : QG Qwen + Codex, 2026-06-10.
- **Refs** :
- `docs/coordination/inbox_codex/2026-06-10_1425_qwen-to-codex-claude-dom_QG-WPC-PATCH2-GO.md`
- `docs/coordination/inbox_codex/2026-06-10_1435_qwen-to-codex-claude-dom_QG-WPC-PATCH3-GO.md`
- `docs/coordination/active/2026-06-10_1437_wpc-patch1-3-go-stop-patch4.md`
- **Decision** : WP-C Patch 1, Patch 2 et Patch 3 sont GO, locaux, non pousses, non deployes.
- **Etat commits** :
- Patch 1 : `f7f692641`.
- Patch 2 : `9fb2c7bfe`.
- Patch 3 : `b20d17882`.
- **Garde-fou** : Patch 4 est le premier impact runtime (`api_stream.py::_verify_token` derriere flag `RPA_FLEET_PER_AGENT_TOKEN`) et reste interdit sans decision Dom explicite.
- **Autorise** : preparation read-only, plan TDD, plan rollback, audit risques.
- **Interdit** : Patch 4 code, push, deploiement, ZIP/package, M2 live, multi-TIM, secret en clair.
### D-2026-06-10-03 : WP-C arrete pour POC, dette auth globale acceptee
- **Tranche par** : Dom, relaye/QG par Qwen a 14:50 CEST.
- **Refs** :
- `docs/coordination/inbox_codex/2026-06-10_1450_qwen-to-codex-claude-dom_DECISION-WPC-ABANDON-DETTE.md`
- `docs/coordination/active/2026-06-10_1440_anti-doublon-wpc-verdict.md`
- `docs/DETTE_TECHNIQUE.md` (`DETTE-016`)
- **Decision** : le token global + `machine_id` auto-declare est suffisant pour le POC controle.
- **Consequence** : WP-C token par poste est arrete pour le POC ; Patch 4 runtime est annule.
- **Etat commits** : Patch 1-3 restent locaux, inertes, non pousses, non deployes.
- **Dette** : `DETTE-016` creee en P2/ACCEPTED pour le gap "auth token global sans verification cryptographique par poste".
- **Garde-fou** : reouvrir WP-C ou une solution equivalente uniquement avant distribution multi-TIM elargie, exposition reseau non maitrisee ou besoin de revocation par poste non contournable.
## Decisions en attente
| Sujet | En attente de | Impact |
|---|---|---|
| M2 Lea live | Dom devant Windows + GO Codex | Execution supervisee |
| Branches/blobs/protection | QG + GO Dom | Hygiene repo post-POC |
| Reouverture WP-C post-POC | Decision Dom explicite | Seulement si multi-TIM elargi / exposition reseau / revocation forte |
— Codex

View File

@@ -0,0 +1,105 @@
# Revue globale post-panne DGX — 2026-06-20
- `Pilote`: Codex
- `Contributeurs`: Claude, Qwen
- `Contexte`: coupure électrique à 02:07 CEST
- `Verdict`: **GO labo supervisé / NO-GO fonctionnement autonome et clinique**
## Résumé
Le DGX, les services, les données et la VM Windows ont survécu ou ont été récupérés. L'accès Dom à la VM est rétabli. La panne révèle cependant que l'accès graphique et le démarrage complet de Léa ne sont pas encore autonomes: le tunnel SSH et le mot de passe VNC sont volatils, et aucun agent Windows n'a encore prouvé une reconnexion après la panne.
## Checklist consolidée
| Domaine | Contrôle | État | Preuve / observation |
|---|---|---:|---|
| Coordination | Watcher Codex/Claude/Qwen | OK | service utilisateur persistant `enabled+active` |
| Réseau | DGX joignable sur `192.168.1.45` | OK | IP WiFi statique, SSH et services accessibles |
| Réseau | Exclusion/réservation `.45` dans le DHCP box | À FAIRE | risque de conflit futur |
| Réseau | Ethernet clinique inactif | OK | interface sans carrier |
| Réseau | Ethernet clinique `autoconnect=off` | KO | profil encore `autoconnect=yes` |
| Sécurité | 5900/5902/3389/22220/8000 fermés depuis le LAN | OK | nmap: filtered/closed; loopback conservé |
| Services | Services système RPA | OK | 10 units `enabled`; services critiques actifs, zéro erreur/restart |
| Dashboard | API et UI | OK | auth 401 sans creds; UI réelle rendue; Socket.IO/API 200 |
| Dashboard | Fallback user concurrent | KO | service user disabled mais actif en crash-loop, 167+ restarts, conflit 5001 |
| Dashboard | Monitoring 5003 / Session Cleaner 5006 | DÉGRADÉ | affichés arrêtés; non bloquants POC mais classification à clarifier |
| Dashboard | Liens navigateur vers `localhost` | DÉGRADÉ | liens 8000/5002/3002/5004/5005 incorrects depuis un poste distant |
| Dashboard | Backups/restore points | DÉGRADÉ | dashboard annonce 0 backup et aucun point de restauration |
| VWB | Backend/frontend | OK | health 200; UI réelle rendue; catalogue/session APIs 200 |
| VWB | Cohérence catalogue | DÉGRADÉ | DB=24 workflows; sélecteur UI=22; review=8 |
| VWB | Workflows appris | DÉGRADÉ | plusieurs entrées importables à 0 nœud/0 transition |
| Données | `workflows.db` | OK | SQLite `integrity_check=ok`, 24 workflows |
| Données | Workflows fichiers | OK | symlink runtime valide, 42 fichiers |
| Données | Anchors | OK | 468 fichiers anchors + 47 anchor images |
| FAISS | Index et métadonnées | OK | 13 666 vecteurs, test HTTP 200 `success=true`, index sain |
| Worker | Daemon et queue | OK | heartbeat post-panne; queue total/pending/failed=0 |
| Agent chat | Service | OK | status online, 79 workflows |
| Streaming | Service | OK | `/health=200`, version 1.0.0 |
| Upload API | Service 8000 | OK | `rpa-vision-v3-api` actif+enabled, 401 loopback attendu |
| Ollama/grounder | Services | OK/DÉGRADÉ | process/services présents; fonction modèle non exercée dans ce smoke |
| VM | Autostart QEMU | OK après recovery | user service active+enabled, linger, 0 restart |
| VM | Boot Windows | OK | framebuffer Windows lock screen, 8,6 Go RSS |
| VM | VNC serveur loopback | OK | RFB 3.8 sur `127.0.0.1:5902` |
| VM | Tunnel poste Dom | RÉTABLI MAIS VOLATIL | tunnel local actif; Remmina connecté; perdu au reboot poste |
| VM | Mot de passe VNC | RÉTABLI MAIS VOLATIL | auth de bout en bout validée; absent du script d'autostart |
| VM | SSH guest 22220 | KO | hostfwd présent mais aucune bannière guest |
| VM | Guest agent QEMU | KO/NON PROUVÉ | socket présent, aucune réponse `guest-ping` |
| Léa Windows | Reconnexion agent post-panne | KO/NON PROUVÉ | aucun `last_seen` fleet postérieur à la panne |
| Léa Windows | `config.txt` live vers `.45` | NON TESTÉ | le template build ne prouve pas la configuration installée |
| Git DGX | Branche | OK | `poc-dgx` |
| Git DGX | Reproductibilité déploiement | DÉGRADÉ | HEAD `ec1fb81`, patch auth `app.py` + DB modifiés, cible `cf81ce4c7` non intégrée |
| Installateur | Artefact autonome 1.0.1 | DÉGRADÉ | seul `deploy/Lea_v1.0.0.zip` visible dans ce checkout |
| Frontend | VWB servi par Vite dev | POC SEULEMENT | port 3002 LAN; à remplacer par build/proxy pour clinique |
## Cause de l'absence d'accès VM
1. Le poste Dom a redémarré: le tunnel SSH local a disparu.
2. Le VNC QEMU est volontairement loopback-only et filtré depuis le LAN.
3. QEMU redémarre avec `password=on`, mais aucun script ne réinjecte le mot de passe.
Correctif runtime appliqué: tunnel `localhost:5902` recréé, mot de passe VNC reposé sans exposition, authentification validée, Remmina connecté.
## Tests fonctionnels nécessitant Dom dans Windows
- [ ] Se connecter à la session Windows.
- [ ] Vérifier que Léa démarre automatiquement; sinon la lancer.
- [ ] Vérifier le `config.txt` live vers le DGX `.45`.
- [ ] Confirmer un nouveau `last_seen` de la machine dans Fleet.
- [ ] Ouvrir « Discuter avec Léa » et envoyer `Bonjour`.
- [ ] Vérifier les bulles/progressions d'une action simple.
- [ ] Importer un workflow JSON dans VWB.
- [ ] Ouvrir une étape avec anchor et confirmer le crop.
- [ ] Exécuter un replay supervisé sans action destructive.
## Actions prioritaires
### P0 — autonomie après reboot
1. Stopper proprement le fallback `rpa-vision-v3-dashboard-user` en crash-loop; garder le service système.
2. Rendre le tunnel VM persistant sur le poste Dom (`systemd --user` ou `autossh`).
3. Rendre l'auth VNC persistante via un secret local protégé, ou retirer le password QEMU et s'appuyer sur SSH+loopback.
4. Après login Windows, prouver le démarrage/re-enrôlement Léa et corriger le `config.txt` live si nécessaire.
### P1 — installation fiable
5. Exclure/réserver `.45` dans le DHCP de la box.
6. Passer le profil Ethernet clinique à `autoconnect=no` tant que non autorisé.
7. Réconcilier Git DGX avec backup de `workflows.db`; ne pas utiliser `reset --hard` sans procédure validée.
8. Produire/identifier l'installateur Léa 1.0.1 attendu.
9. Corriger les liens dashboard `localhost` et distinguer clairement services critiques/optionnels.
10. Mettre en place un vrai backup/restore point post-déploiement.
### P2 — dette produit
11. Diagnostiquer guest SSH/QEMU guest agent.
12. Réconcilier les 24 workflows DB avec les 22 affichés et traiter les 8 reviews/entrées à 0 nœud.
13. Remplacer Vite dev par un build statique/proxy avant clinique.
## Preuves visuelles
- `output/playwright/dgx-post-panne-dashboard-20260620-0241.png`
- `output/playwright/dgx-post-panne-vwb-20260620-0241.png`
## Divergences de revue résolues
La première matrice Qwen confondait `5006` avec l'upload API, inspectait les unités user au lieu des unités système et utilisait des chemins de données non runtime. Les contrôles Codex/Claude ont confirmé: upload API sur 8000, services système persistants, 42 fichiers workflows et 468+47 fichiers anchors. Les constats Qwen conservés sont le guest SSH KO, le fallback dashboard en crash-loop et le worktree DGX non reproductible.

View File

@@ -0,0 +1,14 @@
[Unit]
Description=RPA Vision coordination inbox watcher
After=default.target
[Service]
Type=simple
WorkingDirectory=/home/dom/ai/rpa_vision_v3
Environment=COORD_LOOP_INTERVAL=15
ExecStart=/home/dom/ai/rpa_vision_v3/docs/coordination/coordination_loop.sh watch 15
Restart=always
RestartSec=5
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,21 @@
[Unit]
Description=RPA Vision V3 - Web Dashboard (Flask) user fallback
After=default.target network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/home/aivanov/ai/rpa_vision_v3
EnvironmentFile=/home/aivanov/ai/rpa_vision_v3/.env.local
Environment=PYTHONUNBUFFERED=1
Environment=ENVIRONMENT=production
Environment=RPA_SERVICE_NAME=rpa-vision-v3-dashboard-user
Environment=RPA_GROUNDING_SOCKET=/run/rpa/grounding.sock
Environment=RPA_GROUNDING_IMG_DIR=/run/rpa
ExecStart=/home/aivanov/ai/rpa_vision_v3/venv_v3/bin/python3 web_dashboard/app.py
Restart=on-failure
RestartSec=3
TimeoutStopSec=30
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,69 @@
# Template — Quality Gate (QG)
- `De`: [Agent QG]
- `A`: [Destinataire]
- `Copie`: [CC]
- `Date`: [YYYY-MM-DD HH:MM Europe/Paris]
- `Statut`: QG
- `Répond à`: [Fichier de référence]
---
## Contexte
[1-2 lignes décrivant ce qui est évalué]
---
## Critères d'acceptation
| # | Critère | Requis | Résultat | Verdict |
|---|---|---|---|---|
| 1 | [Description du critère] | ✅ | [Résultat observé] | GO/NO-GO |
| 2 | ... | | | |
| 3 | ... | | | |
---
## Vérifications directes
| Test | Commande / Méthode | Résultat attendu | Résultat observé | Verdict |
|---|---|---|---|---|
| Tests unitaires | `pytest ...` | X/Y passés | [Résultat] | ✅/❌ |
| Smoke test | [Commande] | [Attendu] | [Observé] | ✅/❌ |
| Code review | [Fichiers] | [Critère] | [Observé] | ✅/❌ |
---
## Réserves
| # | Réserve | Impact | Action requise |
|---|---|---|---|
| R1 | [Description] | [Faible/Moyen/Haut] | [Action] |
---
## Verdict global
**[GO / GO avec réserves / NO-GO]**
[1-2 lignes de justification]
---
## Prochaines étapes
| Étape | Owner | Deadline |
|---|---|---|
| [Action] | [Qui] | [Quand] |
---
## Stop conditions
- [Condition qui bloquerait le GO]
- [Autre condition]
---
*Template réutilisable — copier et adapter pour chaque QG.*