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:
Claude committed 2026-10-03 20:28:14 +00:00
1 parent 2c84cfda53
commit 7da4c6d5fd
35 files changed
+1010 -240

No files matched your search

+27 -2
View File
@@ -16,6 +16,14 @@ interface GridState {
displayDensity: "compact" | "normal" | "comfortable";
/** The active URL search parameters (e.g. ?fournisseur=123&magasin=TOTAL) */
activeGridQuery: string;
/**
* Compteur de demandes de rechargement forcé (ignore le cache serveur).
* Volontairement hors URL et non persisté : un paramètre `_refresh` dans
* l'URL restait mémorisé dans `activeGridQuery`, et chaque retour sur la
* Grille par le menu relançait un recalcul complet.
*/
refreshRequest: number;
requestRefresh: () => void;
/** Persisted column visibility state */
columnVisibility: Record<string, boolean>;
/** Persisted column sizing state */
@@ -59,6 +67,15 @@ interface GridState {
let summaryTimer: ReturnType<typeof setTimeout> | null = null;
/** Retire l'ancien paramètre `_refresh` (cache-buster) d'une query string mémorisée. */
function sansParamRefresh(query: string): string {
if (!query.includes("_refresh")) return query;
const params = new URLSearchParams(query.startsWith("?") ? query.slice(1) : query);
params.delete("_refresh");
const qs = params.toString();
return qs ? `?${qs}` : "";
}
function indexRows(rows: ProductRow[]): Record<string, ProductRow> {
const indexed: Record<string, ProductRow> = {};
for (const row of rows) indexed[row.codein] = row;
@@ -117,6 +134,7 @@ export const useGridStore = create<GridState>()(
},
displayDensity: "normal",
activeGridQuery: "",
refreshRequest: 0,
columnVisibility: {},
columnSizing: {},
showMonthlySales: true,
@@ -202,7 +220,8 @@ export const useGridStore = create<GridState>()(
set((state) => ({ ...state, filters: { ...state.filters, code3: codes } }));
},
setDisplayDensity: (density) => set({ displayDensity: density }),
setActiveGridQuery: (query) => set({ activeGridQuery: query }),
setActiveGridQuery: (query) => set({ activeGridQuery: sansParamRefresh(query) }),
requestRefresh: () => set((state) => ({ refreshRequest: state.refreshRequest + 1 })),
restoreSnapshot: (changes) => {
set({ draftChanges: changes, summary: computeSummary(get().rows, changes, get().activeMagasin) });
},
@@ -249,8 +268,11 @@ export const useGridStore = create<GridState>()(
* dans le navigateur restait une chaîne : `new Set("320211")` produit un
* ensemble de caractères, plus aucune ligne ne correspond, et la Grille
* apparaît vide. Le numéro de version force la reprise de l'existant.
*
* v2 — `activeGridQuery` pouvait contenir `_refresh=…` : le lien « Grille »
* du menu forçait alors un recalcul serveur complet à chaque retour.
*/
version: 1,
version: 2,
migrate: (persisted: unknown, version: number): GridPersisted => {
const etat = (persisted ?? {}) as GridPersisted & { filters?: Record<string, unknown> };
if (version < 1 && etat.filters) {
@@ -262,6 +284,9 @@ export const useGridStore = create<GridState>()(
// à la fusion (le bloc disparaîtrait sans raison).
if (typeof etat.showMonthlySales !== "boolean") etat.showMonthlySales = true;
if (typeof etat.showPrices !== "boolean") etat.showPrices = true;
if (typeof etat.activeGridQuery === "string") {
etat.activeGridQuery = sansParamRefresh(etat.activeGridQuery);
}
return etat;
},
// Only persist filters, display density, drafts, active grid query, and column visibility/sizing