From 136bc345d9e45dd688d04ba2b7565798f458245c Mon Sep 17 00:00:00 2001 From: SOC Analyst Date: Thu, 19 Mar 2026 11:32:52 +0100 Subject: [PATCH] =?UTF-8?q?fix(clustering):=20animation=20visible,=20param?= =?UTF-8?q?s=20persist=C3=A9s,=20bouton=20toujours=20actif?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 1 — Animation invisible (WebGL canvas ignore z-index): - DeckGL n'est plus rendu pendant computing||loading - Structure ternaire : animation | état vide | DeckGL (mutuellement exclusifs) - Le canvas WebGL n'est monté que quand des données sont disponibles - Animation garantie visible car aucun élément WebGL ne la couvre Bug 2 — Bouton 'Recalculer' inactif pendant computing: - disabled={loading} seulement (plus disabled pendant computing) - L'utilisateur peut relancer pendant un calcul en cours - Le texte du bouton indique l'état : 'Calcul en cours…' / 'Chargement…' / 'Recalculer' Bug 3 — Paramètres perdus au rechargement: - loadParams() lit les params depuis localStorage (clé: soc_clustering_params) - useState initialisé depuis loadParams() au montage du composant - useEffect sauvegarde k, hours, sensitivity dans localStorage à chaque changement - Les réglages (k, sensibilité, fenêtre) survivent aux rechargements Fix stale closure: - sensitivity ajouté aux dépendances de useCallback fetchClusters - Évite d'envoyer une ancienne valeur de sensitivity à l'API Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- frontend/src/components/ClusteringView.tsx | 109 ++++++++++++--------- 1 file changed, 63 insertions(+), 46 deletions(-) diff --git a/frontend/src/components/ClusteringView.tsx b/frontend/src/components/ClusteringView.tsx index 3b9bebe..fdf2e93 100644 --- a/frontend/src/components/ClusteringView.tsx +++ b/frontend/src/components/ClusteringView.tsx @@ -93,10 +93,21 @@ function hexToRgba(hex: string, alpha = 255): [number, number, number, number] { // ─── Composant principal ────────────────────────────────────────────────────── +// Persistence des paramètres dans localStorage +const LS_KEY = 'soc_clustering_params'; +function loadParams() { + try { + const s = localStorage.getItem(LS_KEY); + if (s) return JSON.parse(s) as { k: number; hours: number; sensitivity: number }; + } catch { /* ignore */ } + return { k: 20, hours: 24, sensitivity: 1.0 }; +} + export default function ClusteringView() { - const [k, setK] = useState(20); - const [hours, setHours] = useState(24); - const [sensitivity, setSensitivity] = useState(1.0); + const init = loadParams(); + const [k, setK] = useState(init.k); + const [hours, setHours] = useState(init.hours); + const [sensitivity, setSensitivity] = useState(init.sensitivity); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [computing, setComputing] = useState(false); @@ -117,9 +128,15 @@ export default function ClusteringView() { maxZoom: 6, }); + // ── Persistence des paramètres ────────────────────────────────────────── + useEffect(() => { + localStorage.setItem(LS_KEY, JSON.stringify({ k, hours, sensitivity })); + }, [k, hours, sensitivity]); + // ── Chargement / polling ───────────────────────────────────────────────── const fetchClusters = useCallback(async (force = false) => { + if (pollRef.current) { clearTimeout(pollRef.current); pollRef.current = null; } setLoading(true); setError(null); try { @@ -128,7 +145,7 @@ export default function ClusteringView() { }); if (res.data.status === 'computing' || res.data.status === 'idle') { setComputing(true); - // Polling + // Polling toutes les 3s pollRef.current = setTimeout(() => fetchClusters(), 3000); } else { setComputing(false); @@ -139,10 +156,10 @@ export default function ClusteringView() { const ys = res.data.nodes.map(n => toWorld(n.pca_y)); const minX = Math.min(...xs), maxX = Math.max(...xs); const minY = Math.min(...ys), maxY = Math.max(...ys); - const pad = 0.18; // 18% de marge de chaque côté + const pad = 0.18; const fitW = (maxX - minX) * (1 + 2 * pad) || WORLD; const fitH = (maxY - minY) * (1 + 2 * pad) || WORLD; - const canvasW = window.innerWidth - 288 - (selected ? 384 : 0); // panel + sidebar + const canvasW = window.innerWidth - 288 - (selected ? 384 : 0); const canvasH = window.innerHeight - 60; setViewState(v => ({ ...v, @@ -160,7 +177,7 @@ export default function ClusteringView() { } finally { setLoading(false); } - }, [k, hours]); + }, [k, hours, sensitivity]); // sensitivity inclus pour éviter la stale closure useEffect(() => { fetchClusters(); @@ -387,9 +404,9 @@ export default function ClusteringView() { Afficher les arêtes @@ -442,12 +459,12 @@ export default function ClusteringView() { {/* ── Canvas WebGL (deck.gl) ── */}
- {/* Animation de calcul — remplace le canvas pendant le traitement */} - {(computing || loading) && ( -
+ {/* Animation de calcul — REMPLACE DeckGL (le canvas WebGL ignore z-index) */} + {(computing || loading) ? ( +
{/* Noeuds pulsants animés */}
- {/* Anneau tournant */} + {/* Anneaux tournants */}
@@ -457,9 +474,8 @@ export default function ClusteringView() { {/* Noeuds orbitaux représentant les clusters */} {([0,1,2,3,4,5,6,7] as const).map((i) => { const angle = (i / 8) * 2 * Math.PI; - const r = 88; - const x = 50 + (r / 1.12) * Math.cos(angle); - const y = 50 + (r / 1.12) * Math.sin(angle); + const x = 50 + 39 * Math.cos(angle); + const y = 50 + 39 * Math.sin(angle); const colors = ['#dc2626','#f97316','#eab308','#22c55e','#3b82f6','#8b5cf6','#ec4899','#14b8a6']; return (

Clustering en cours…

-

K-means++ · 31 features · toutes les IPs

+

+ K-means++ · 31 features · {Math.round(k * sensitivity)} clusters · toutes les IPs +

Mise à jour automatique toutes les 3 secondes

- )} - - {/* Message état vide */} - {!data && !loading && !computing && ( + ) : !data ? ( + /* État vide initial */
🔬 Cliquez sur Recalculer pour démarrer
- )} - - setViewState(vs as typeof viewState)} - layers={layers as any} - style={{ width: '100%', height: '100%' }} - controller={true} - > - {/* Légende overlay */} -
-
- {[['#dc2626', 'CRITICAL'], ['#f97316', 'HIGH'], ['#eab308', 'MEDIUM'], ['#22c55e', 'LOW']].map(([c, l]) => ( -
- - {l} -
- ))} + ) : ( + /* Canvas WebGL — monté seulement quand il y a des données */ + setViewState(vs as typeof viewState)} + layers={layers as any} + style={{ width: '100%', height: '100%' }} + controller={true} + > + {/* Légende overlay */} +
+
+ {[['#dc2626', 'CRITICAL'], ['#f97316', 'HIGH'], ['#eab308', 'MEDIUM'], ['#22c55e', 'LOW']].map(([c, l]) => ( +
+ + {l} +
+ ))} +
-
- {/* Tooltip zoom hint */} -
-
Scroll pour zoomer · Drag pour déplacer · Click sur un cluster
-
- + {/* Tooltip zoom hint */} +
+
Scroll pour zoomer · Drag pour déplacer · Click sur un cluster
+
+ + )}
{/* ── Sidebar droite (sélection) ── */}