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

+17 -8
View File
@@ -16,6 +16,15 @@ const CONFIG_FILE = path.join(process.cwd(), "data", ".db-config.json");
const FF_API_BASE_DEFAULT = "https://api.ffnancy.fr";
const FF_API_BASE_TTL_MS = 30_000;
/**
* Options de tout appel à l'API FF : jamais de cache HTTP, et un délai maximal.
* Sans délai, une API muette figeait indéfiniment la page qui l'attendait.
*/
const FF_FETCH_TIMEOUT_MS = Number(process.env.FF_API_TIMEOUT_MS) || 30_000;
export function ffFetchInit(): RequestInit {
return { cache: "no-store", signal: AbortSignal.timeout(FF_FETCH_TIMEOUT_MS) };
}
let ffApiBaseCache: { value: string; at: number } | null = null;
/**
@@ -201,7 +210,7 @@ async function fetchAllPages<T>(
// Page 1 — récupère les items et tente d'obtenir le total pour paralléliser
const url1 = buildUrl(1);
console.log(`[api-ff] GET ${url1}`);
const res1 = await fetch(url1, { cache: "no-store" });
const res1 = await fetch(url1, ffFetchInit());
if (!res1.ok) {
console.error(`[api-ff] HTTP ${res1.status} — ${url1}`);
return [];
@@ -227,7 +236,7 @@ async function fetchAllPages<T>(
const results = await Promise.all(chunk.map(async (p) => {
const url = buildUrl(p);
console.log(`[api-ff] GET ${url}`);
const res = await fetch(url, { cache: "no-store" });
const res = await fetch(url, ffFetchInit());
if (!res.ok) return [] as T[];
const data = await res.json();
return extractList(data) as T[];
@@ -245,7 +254,7 @@ async function fetchAllPages<T>(
const results = await Promise.all(chunk.map(async (p) => {
const url = buildUrl(p);
console.log(`[api-ff] GET ${url}`);
const res = await fetch(url, { cache: "no-store" });
const res = await fetch(url, ffFetchInit());
if (!res.ok) return { p, items: [] as T[] };
const data = await res.json();
return { p, items: extractList(data) as T[] };
@@ -311,7 +320,7 @@ export async function getFournisseursFromApi(
: `${FF_API_BASE}/api/fournisseurs?limit=500`;
console.log(`[api-ff] GET ${url}`);
const res = await fetch(url, { cache: "no-store" });
const res = await fetch(url, ffFetchInit());
if (!res.ok) throw new Error(`HTTP ${res.status} on ${url}`);
const data = await res.json();
@@ -468,7 +477,7 @@ export async function getMensuelByArticles(
if (!noId) return;
try {
const url = `${FF_API_BASE}/api/articles/${encodeURIComponent(String(noId))}/mensuel?dateDebut=${dateDebut}&dateFin=${dateFin}`;
const res = await fetch(url, { cache: "no-store" });
const res = await fetch(url, ffFetchInit());
if (!res.ok) return;
const data = await res.json();
const entries: FfMensuelEntry[] = Array.isArray(data.data) ? data.data : [];
@@ -555,7 +564,7 @@ export async function getReferentielByArticles(
if (!noId) return;
try {
const url = `${FF_API_BASE}/api/articles/${encodeURIComponent(String(noId))}/referentiel`;
const res = await fetch(url, { cache: "no-store" });
const res = await fetch(url, ffFetchInit());
if (!res.ok) return;
const data: FfReferentiel = await res.json();
if (data?.article) result.set(art.codein, data);
@@ -581,7 +590,7 @@ export async function getCommandesByFournisseur(
try {
const res = await fetch(
`${FF_API_BASE}/api/commandes/articles?codefou=${encodeURIComponent(codefou)}`,
{ cache: "no-store" }
ffFetchInit()
);
if (!res.ok) return result;
const data = await res.json();
@@ -602,7 +611,7 @@ export async function getCommandesByFournisseur(
export async function getSyncStatus(): Promise<FfSyncStatus | null> {
try {
const FF_API_BASE = await getFfApiBase();
const res = await fetch(`${FF_API_BASE}/api/sync/status`, { cache: "no-store" });
const res = await fetch(`${FF_API_BASE}/api/sync/status`, ffFetchInit());
if (!res.ok) return null;
return normalizeSyncStatus(await res.json());
} catch (err) {