From e3efef2fe7edb408e68a18f55554310cc7f6424b Mon Sep 17 00:00:00 2001 From: Dom Date: Sun, 19 Apr 2026 00:04:30 +0200 Subject: [PATCH] =?UTF-8?q?fix(vwb):=20noms=20workflows=20lisibles=20+=20b?= =?UTF-8?q?iblioth=C3=A8que=20captures=20persistante?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CSS : le dropdown héritait color:white du header → forcé #212121 sur .workflow-dropdown et .dropdown-item .item-name Bibliothèque : migration localStorage → backend (capture_library.json) - GET/POST /api/v3/capture/library (max 50 captures) - loadLibraryAsync() charge depuis backend, fallback localStorage - saveLibrary() écrit dans les deux (localStorage + backend) - capture_library.json gitignored Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 1 + .../backend/api_v3/capture.py | 79 ++++++++++++++++++ .../src/components/CaptureLibrary.tsx | 27 +++--- .../src/components/CapturePanel.tsx | 27 +++--- .../frontend_v4/src/services/api.ts | 21 +++++ .../src/services/captureLibraryStorage.ts | 82 ++++++++++++++++--- .../frontend_v4/src/styles.css | 6 +- 7 files changed, 205 insertions(+), 38 deletions(-) diff --git a/.gitignore b/.gitignore index 6bf09b973..cc1580401 100644 --- a/.gitignore +++ b/.gitignore @@ -95,6 +95,7 @@ archives/ # === Données runtime (sessions, learning, buffer, config local) === data/ +**/capture_library.json .hypothesis/ .deps_installed # Buffers SQLite locaux (streamer, cache) diff --git a/visual_workflow_builder/backend/api_v3/capture.py b/visual_workflow_builder/backend/api_v3/capture.py index 12661437c..114dec374 100644 --- a/visual_workflow_builder/backend/api_v3/capture.py +++ b/visual_workflow_builder/backend/api_v3/capture.py @@ -5,10 +5,13 @@ Gestion des captures d'écran et création d'ancres visuelles POST /api/v3/capture/screen → Capture écran POST /api/v3/capture/select → Crée ancre depuis sélection GET /api/v3/anchor/{id}/image → Image de l'ancre +GET /api/v3/capture/library → Charge la bibliothèque de captures +POST /api/v3/capture/library → Sauvegarde la bibliothèque de captures """ from flask import jsonify, request, send_file from datetime import datetime +import json import uuid import os import base64 @@ -22,6 +25,10 @@ from db.models import db, Step, VisualAnchor, get_session_state ANCHORS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'anchors') os.makedirs(ANCHORS_DIR, exist_ok=True) +# Fichier pour la bibliothèque de captures (persistance disque) +DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data') +CAPTURE_LIBRARY_PATH = os.path.join(DATA_DIR, 'capture_library.json') + def generate_id(prefix: str) -> str: """Génère un ID unique""" @@ -316,3 +323,75 @@ def get_anchor_base64(anchor_id: str): 'success': False, 'error': str(e) }), 500 + + +# ── Bibliothèque de captures (persistance disque) ──────────────────────── + +@api_v3_bp.route('/capture/library', methods=['GET']) +def get_capture_library(): + """ + Charge la bibliothèque de captures depuis le disque. + + Response: + { + "success": true, + "library": [ { id, capture, timestamp, sessionId, favorite }, ... ] + } + """ + try: + if os.path.exists(CAPTURE_LIBRARY_PATH): + with open(CAPTURE_LIBRARY_PATH, 'r', encoding='utf-8') as f: + library = json.load(f) + else: + library = [] + + return jsonify({ + 'success': True, + 'library': library + }) + + except Exception as e: + print(f"⚠️ [CaptureLibrary] Erreur lecture: {e}") + return jsonify({ + 'success': True, + 'library': [] + }) + + +@api_v3_bp.route('/capture/library', methods=['POST']) +def save_capture_library(): + """ + Sauvegarde la bibliothèque de captures sur disque. + + Request: + { + "library": [ { id, capture, timestamp, sessionId, favorite }, ... ] + } + + Response: + { + "success": true, + "count": 5 + } + """ + try: + data = request.get_json() or {} + library = data.get('library', []) + + # Limiter à 50 captures pour éviter un fichier trop gros + library = library[:50] + + with open(CAPTURE_LIBRARY_PATH, 'w', encoding='utf-8') as f: + json.dump(library, f, ensure_ascii=False) + + return jsonify({ + 'success': True, + 'count': len(library) + }) + + except Exception as e: + print(f"⚠️ [CaptureLibrary] Erreur écriture: {e}") + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 diff --git a/visual_workflow_builder/frontend_v4/src/components/CaptureLibrary.tsx b/visual_workflow_builder/frontend_v4/src/components/CaptureLibrary.tsx index ad05e53f0..315ba69b9 100644 --- a/visual_workflow_builder/frontend_v4/src/components/CaptureLibrary.tsx +++ b/visual_workflow_builder/frontend_v4/src/components/CaptureLibrary.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import type { Capture } from '../types'; import { - loadLibrary, + loadLibraryAsync, saveLibrary, compressThumbnail, } from '../services/captureLibraryStorage'; @@ -33,19 +33,20 @@ export default function CaptureLibrary({ currentCapture, onSelectCapture, onCapt const [viewMode, setViewMode] = useState<'all' | 'session' | 'favorites'>('session'); const [selectedItems, setSelectedItems] = useState>(new Set()); - // Charger la bibliothèque depuis localStorage (persiste entre onglets/sessions). - // Le helper loadLibrary() gère la migration des anciennes clés et de sessionStorage. + // Charger la bibliothèque depuis le backend (prioritaire), fallback localStorage. + // Le helper loadLibraryAsync() gère la migration des anciennes clés et de sessionStorage. useEffect(() => { - const loaded = loadLibrary(currentSessionId) as LibraryItem[]; - setLibrary( - loaded.map((item) => ({ - ...item, - timestamp: - typeof item.timestamp === 'string' - ? new Date(item.timestamp) - : item.timestamp, - })) - ); + loadLibraryAsync(currentSessionId).then((loaded) => { + setLibrary( + loaded.map((item) => ({ + ...item, + timestamp: + typeof item.timestamp === 'string' + ? new Date(item.timestamp) + : item.timestamp, + })) + ); + }); }, [currentSessionId]); // Sauvegarder la bibliothèque (localStorage + gestion de quota) diff --git a/visual_workflow_builder/frontend_v4/src/components/CapturePanel.tsx b/visual_workflow_builder/frontend_v4/src/components/CapturePanel.tsx index ffebf64e9..460ae7c4d 100644 --- a/visual_workflow_builder/frontend_v4/src/components/CapturePanel.tsx +++ b/visual_workflow_builder/frontend_v4/src/components/CapturePanel.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from 'react'; import type { Capture, ExecutionMode } from '../types'; import type { UIElement } from '../services/uiDetection'; import { - loadLibrary, + loadLibraryAsync, saveLibrary, compressThumbnail, } from '../services/captureLibraryStorage'; @@ -61,19 +61,20 @@ export default function CapturePanel({ const isDebugMode = executionMode === 'debug'; - // Charger la bibliothèque depuis localStorage (clé unifiée 'captureLibrary_v2'). - // Le helper loadLibrary() migre aussi les données de l'ancienne clé 'captureLibrary'. + // Charger la bibliothèque depuis le backend (prioritaire), fallback localStorage. + // Le helper loadLibraryAsync() migre aussi les données de l'ancienne clé 'captureLibrary'. useEffect(() => { - const loaded = loadLibrary() as LibraryItem[]; - setLibrary( - loaded.map((item) => ({ - ...item, - timestamp: - typeof item.timestamp === 'string' - ? new Date(item.timestamp) - : item.timestamp, - })) - ); + loadLibraryAsync().then((loaded) => { + setLibrary( + loaded.map((item) => ({ + ...item, + timestamp: + typeof item.timestamp === 'string' + ? new Date(item.timestamp) + : item.timestamp, + })) + ); + }); }, []); // Sauvegarder la bibliothèque (localStorage + gestion de quota) diff --git a/visual_workflow_builder/frontend_v4/src/services/api.ts b/visual_workflow_builder/frontend_v4/src/services/api.ts index 0fed9898e..71a3e857d 100644 --- a/visual_workflow_builder/frontend_v4/src/services/api.ts +++ b/visual_workflow_builder/frontend_v4/src/services/api.ts @@ -358,3 +358,24 @@ export async function exportForLea( machine_id: machineId, }); } + +// Bibliothèque de captures — persistance serveur +export interface CaptureLibraryItem { + id: string; + capture: Capture; + timestamp: string; + sessionId?: string; + favorite?: boolean; +} + +export async function getCaptureLibrary(): Promise<{ + library: CaptureLibraryItem[]; +}> { + return request('GET', '/capture/library'); +} + +export async function saveCaptureLibrary(library: CaptureLibraryItem[]): Promise<{ + count: number; +}> { + return request('POST', '/capture/library', { library }); +} diff --git a/visual_workflow_builder/frontend_v4/src/services/captureLibraryStorage.ts b/visual_workflow_builder/frontend_v4/src/services/captureLibraryStorage.ts index 7ce941395..2780757a0 100644 --- a/visual_workflow_builder/frontend_v4/src/services/captureLibraryStorage.ts +++ b/visual_workflow_builder/frontend_v4/src/services/captureLibraryStorage.ts @@ -11,13 +11,19 @@ * 2. Deux listes désynchronisées (clés différentes). * * Ce module centralise : - * - localStorage (persiste entre sessions) + * - Stockage principal : backend API (fichier JSON sur disque) + * - Cache local : localStorage (fallback si le backend est indisponible) * - clé unique 'captureLibrary_v2' * - compression JPEG 80% / max 320×240 des thumbnails avant stockage * pour rester sous le quota navigateur (typiquement 5–10 MB). + * + * MISE À JOUR (19 avril 2026) : + * Migration du stockage vers le backend pour survivre aux changements + * de port, vidages de cache navigateur et Ctrl+Shift+R. */ import type { Capture } from '../types'; +import { getCaptureLibrary, saveCaptureLibrary } from './api'; export interface LibraryItem { id: string; @@ -34,8 +40,51 @@ const THUMB_MAX_HEIGHT = 240; const THUMB_QUALITY = 0.8; /** - * Charge la bibliothèque depuis localStorage. Migre depuis sessionStorage - * et l'ancienne clé 'captureLibrary' si présentes. + * Charge la bibliothèque depuis le backend (prioritaire), avec fallback + * sur localStorage si le backend est indisponible. + * Migre les anciennes clés sessionStorage/localStorage au premier appel. + */ +export async function loadLibraryAsync(defaultSessionId?: string): Promise { + // 1) Essayer de charger depuis le backend + try { + const data = await getCaptureLibrary(); + if (data.library && data.library.length > 0) { + const items = data.library.map((item) => ({ + ...item, + timestamp: + typeof item.timestamp === 'string' + ? new Date(item.timestamp) + : item.timestamp, + })) as LibraryItem[]; + + // Mettre à jour le cache localStorage + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(data.library)); + } catch { + // Quota dépassé, pas grave — le backend est la source de vérité + } + + return items; + } + } catch (e) { + console.warn('[CaptureLibrary] Backend indisponible, fallback localStorage', e); + } + + // 2) Fallback : charger depuis localStorage (fonction synchrone existante) + const local = loadLibrary(defaultSessionId); + + // 3) Si on a trouvé des données locales mais pas sur le backend, + // pousser vers le backend pour la prochaine fois + if (local.length > 0) { + saveLibrary(local); // Sauvegarde async vers le backend + } + + return local; +} + +/** + * Charge la bibliothèque depuis localStorage (synchrone). + * Migre depuis sessionStorage et l'ancienne clé 'captureLibrary' si présentes. */ export function loadLibrary(defaultSessionId?: string): LibraryItem[] { // 1) Clé principale en localStorage @@ -99,24 +148,25 @@ export function loadLibrary(defaultSessionId?: string): LibraryItem[] { } /** - * Sauvegarde la bibliothèque dans localStorage. Gère les erreurs de quota - * en élaguant les items les plus anciens jusqu'à ce que ça passe. + * Sauvegarde la bibliothèque dans localStorage ET sur le backend. + * Gère les erreurs de quota localStorage en élaguant les items les plus anciens. */ export function saveLibrary(library: LibraryItem[]): void { let toStore = library; - // Jusqu'à 5 tentatives : si QuotaExceededError, on tronque de moitié. + + // Sauvegarder dans localStorage (cache local) for (let attempt = 0; attempt < 5; attempt++) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore)); - return; + break; } catch (e: any) { const isQuota = e?.name === 'QuotaExceededError' || e?.code === 22 || e?.code === 1014; // Firefox if (!isQuota) { - console.error('[CaptureLibrary] Erreur save', e); - return; + console.error('[CaptureLibrary] Erreur save localStorage', e); + break; } // Garder la moitié la plus récente const half = Math.max(1, Math.floor(toStore.length / 2)); @@ -126,7 +176,19 @@ export function saveLibrary(library: LibraryItem[]): void { toStore = toStore.slice(0, half); } } - console.error('[CaptureLibrary] Impossible de sauvegarder même après élagage'); + + // Sauvegarder sur le backend (fire-and-forget, pas de blocage) + saveCaptureLibrary(library.map((item) => ({ + id: item.id, + capture: item.capture, + timestamp: typeof item.timestamp === 'object' && item.timestamp instanceof Date + ? item.timestamp.toISOString() + : String(item.timestamp), + sessionId: item.sessionId, + favorite: item.favorite, + }))).catch((e) => { + console.warn('[CaptureLibrary] Échec sauvegarde backend', e); + }); } /** diff --git a/visual_workflow_builder/frontend_v4/src/styles.css b/visual_workflow_builder/frontend_v4/src/styles.css index a6e55ddc7..b74713c08 100644 --- a/visual_workflow_builder/frontend_v4/src/styles.css +++ b/visual_workflow_builder/frontend_v4/src/styles.css @@ -184,7 +184,7 @@ body { } .workflow-list li.active .wf-name { - color: white; + color: white !important; } .workflow-list .wf-main-row { @@ -197,7 +197,7 @@ body { .workflow-list .wf-name { flex: 1; font-weight: 500; - color: var(--text-primary); + color: #212121 !important; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -2382,6 +2382,7 @@ body { min-width: 320px; max-width: 400px; background: var(--bg-paper); + color: var(--text-primary); border-radius: 8px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); z-index: 1000; @@ -2474,6 +2475,7 @@ body { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + color: #212121 !important; } .dropdown-item .item-meta {