fix(vwb): noms workflows lisibles + bibliothèque captures persistante
Some checks failed
security-audit / Bandit (scan statique) (push) Successful in 11s
security-audit / pip-audit (CVE dépendances) (push) Successful in 11s
security-audit / Scan secrets (grep) (push) Successful in 8s
tests / Lint (ruff + black) (push) Successful in 14s
tests / Tests unitaires (sans GPU) (push) Failing after 14s
tests / Tests sécurité (critique) (push) Has been skipped

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) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-04-19 00:04:30 +02:00
parent 95fddeebb3
commit e3efef2fe7
7 changed files with 205 additions and 38 deletions

View File

@@ -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<Set<string>>(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)

View File

@@ -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)

View File

@@ -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 });
}

View File

@@ -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 510 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<LibraryItem[]> {
// 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);
});
}
/**

View File

@@ -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 {