mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-22 22:48:08 +02:00
perf: lot 1 — chargements plus rapides et corrections, sans changement visuel
Grille (navigateur) - « Rafraîchir » ne passe plus par l'URL : le paramètre _refresh restait mémorisé et chaque retour par le menu forçait un recalcul serveur complet (migration du store pour nettoyer les valeurs déjà enregistrées). - Lignes reçues regroupées toutes les 300 ms au lieu de reconstruire tout le tableau à chaque paquet de 150 (chargement quadratique). - Sélection indexée par code article (getRowId) : après un filtre, l'action groupée visait d'autres produits. Seules les lignes affichées comptent. - Recherche : un passage par ligne au lieu d'un par colonne ; formateurs de nombres partagés (lib/format.ts) ; colonnes mensuelles mémorisées ; plus de transition-all sur les lignes positionnées par transform. - exceljs, jspdf et xlsx chargés au clic seulement. Grille (serveur) - Requêtes Qlik et snapshot lancées en parallèle de la phase SQL. - Cache mémoire borné (LRU) ; calcul en cours réutilisé même en forcé ; flux interrompu quand le client part. - Après un enregistrement, les gammes sont reportées dans le cache et dans grid_rows au lieu d'invalider (recalcul de ~40 s évité). Données et API - lib/ff-cache.ts : cache mémoire 30 min des lectures FF (stock, hit-parade, CA mensuel, fournisseurs, dernière réception, publicités), vidé en fin de synchro nocturne ; erreurs jamais gardées. - Pool PostgreSQL unique (globalThis), réglage sans workers parallèles posé une fois par connexion au lieu d'une transaction autour de chaque requête. - Délai maximal sur tous les appels à l'API FF. - Index session_snapshots ; commandes : plus de double rechargement ; synchro : saisies regroupées ; paramètres : configuration lue une fois ; journal de capture en ajout seul, rien de formaté sans capture ouverte. Corrections - Historique : l'échec de chargement s'affiche (au lieu de « Aucun snapshot »). - Sessions et validations enregistrent le magasin affiché, plus « TOTAL ». - Publicités : libellé du statut « passées ». Plan complet : docs/plan-optimisation-webui.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AcA129WzGTH6zxPHFBGWmn
This commit is contained in:
35 files changed
+1010
-240
No files matched your search
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
CalendarClock, Play, Square, RefreshCw, Loader2, AlertTriangle,
|
||||
CheckCircle, Database, Network, Search,
|
||||
@@ -114,6 +114,10 @@ export function SynchronisationClient() {
|
||||
return () => clearInterval(id);
|
||||
}, [state?.enCours, rafraichirEtat]);
|
||||
|
||||
// Saisie numérique pas encore envoyée (cf. majSettingsDifferee).
|
||||
const patchEnAttente = useRef<Partial<SyncSettings>>({});
|
||||
const minuteurPatch = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const majSettings = async (patch: Partial<SyncSettings>) => {
|
||||
setErreur(null);
|
||||
try {
|
||||
@@ -122,10 +126,26 @@ export function SynchronisationClient() {
|
||||
});
|
||||
const d = await res.json();
|
||||
if (!res.ok) throw new Error(d.error ?? `Erreur ${res.status}`);
|
||||
setSettings(d.settings);
|
||||
// Une saisie faite pendant l'envoi ne doit pas être écrasée par la réponse.
|
||||
setSettings({ ...d.settings, ...patchEnAttente.current });
|
||||
} catch (e) { setErreur(e instanceof Error ? e.message : String(e)); }
|
||||
};
|
||||
|
||||
// Champs numériques : la valeur s'affiche tout de suite et l'envoi au serveur
|
||||
// est regroupé 600 ms après la dernière frappe. Avant, chaque chiffre tapé
|
||||
// partait en PATCH, et une réponse tardive pouvait écraser la saisie en cours.
|
||||
const majSettingsDifferee = (patch: Partial<SyncSettings>) => {
|
||||
setSettings((prev) => (prev ? { ...prev, ...patch } : prev));
|
||||
patchEnAttente.current = { ...patchEnAttente.current, ...patch };
|
||||
if (minuteurPatch.current) clearTimeout(minuteurPatch.current);
|
||||
minuteurPatch.current = setTimeout(() => {
|
||||
const aEnvoyer = patchEnAttente.current;
|
||||
patchEnAttente.current = {};
|
||||
minuteurPatch.current = null;
|
||||
void majSettings(aEnvoyer);
|
||||
}, 600);
|
||||
};
|
||||
|
||||
const action = async (act: "start" | "stop" | "seed", forcer = false) => {
|
||||
setErreur(null);
|
||||
try {
|
||||
@@ -207,7 +227,7 @@ export function SynchronisationClient() {
|
||||
<div>
|
||||
<label className="block text-[11px] mb-1" style={{ color: "var(--text-muted)" }}>De</label>
|
||||
<input type="number" min={0} max={23} value={settings.heureDebut}
|
||||
onChange={(e) => majSettings({ heureDebut: Number(e.target.value) })}
|
||||
onChange={(e) => majSettingsDifferee({ heureDebut: Number(e.target.value) })}
|
||||
className="w-16 rounded-lg px-2 py-1.5 text-[13px]"
|
||||
style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)", color: "var(--text-primary)" }} />
|
||||
</div>
|
||||
@@ -215,7 +235,7 @@ export function SynchronisationClient() {
|
||||
<div>
|
||||
<label className="block text-[11px] mb-1" style={{ color: "var(--text-muted)" }}>À</label>
|
||||
<input type="number" min={0} max={23} value={settings.heureFin}
|
||||
onChange={(e) => majSettings({ heureFin: Number(e.target.value) })}
|
||||
onChange={(e) => majSettingsDifferee({ heureFin: Number(e.target.value) })}
|
||||
className="w-16 rounded-lg px-2 py-1.5 text-[13px]"
|
||||
style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)", color: "var(--text-primary)" }} />
|
||||
</div>
|
||||
@@ -225,14 +245,14 @@ export function SynchronisationClient() {
|
||||
<div>
|
||||
<label className="block text-[11px] mb-1" style={{ color: "var(--text-muted)" }}>Qlik / nuit</label>
|
||||
<input type="number" min={0} max={200} value={settings.qlikParNuit}
|
||||
onChange={(e) => majSettings({ qlikParNuit: Number(e.target.value) })}
|
||||
onChange={(e) => majSettingsDifferee({ qlikParNuit: Number(e.target.value) })}
|
||||
className="w-20 rounded-lg px-2 py-1.5 text-[13px]"
|
||||
style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)", color: "var(--text-primary)" }} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[11px] mb-1" style={{ color: "var(--text-muted)" }}>Qlik si + vieux que</label>
|
||||
<input type="number" min={0} max={365} value={settings.qlikMinJours}
|
||||
onChange={(e) => majSettings({ qlikMinJours: Number(e.target.value) })}
|
||||
onChange={(e) => majSettingsDifferee({ qlikMinJours: Number(e.target.value) })}
|
||||
className="w-20 rounded-lg px-2 py-1.5 text-[13px]"
|
||||
style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)", color: "var(--text-primary)" }} />
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user