mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-22 06:28:09 +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
+14
-22
@@ -9,6 +9,7 @@
|
||||
|
||||
import { db } from "@/db";
|
||||
import { sql, type SQL } from "drizzle-orm";
|
||||
import { ffFetchInit } from "@/lib/api-ff-client";
|
||||
|
||||
const FF_API_BASE = process.env.FF_API_BASE_URL ?? "https://api.ffnancy.fr";
|
||||
|
||||
@@ -20,16 +21,13 @@ let _nomenclatureParentCol: string | null | undefined = undefined; // undefined
|
||||
// Utilitaire : exécuter une requête sans workers parallèles PostgreSQL.
|
||||
// Les workers parallèles consomment /dev/shm (mémoire partagée Docker) et font
|
||||
// échouer les requêtes si le conteneur a un shm_size insuffisant.
|
||||
// SET LOCAL s'applique uniquement à la transaction courante.
|
||||
// Le réglage est désormais posé une fois par connexion du pool (src/db/index.ts) :
|
||||
// la transaction BEGIN / SET LOCAL / COMMIT qui entourait chaque requête coûtait
|
||||
// trois allers-retours de plus à chacune.
|
||||
// ---------------------------------------------------------------------------
|
||||
async function pgNoParallel(query: SQL): Promise<{ rows: unknown[] }> {
|
||||
let rows: unknown[] = [];
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.execute(sql`SET LOCAL max_parallel_workers_per_gather = 0`);
|
||||
const r = await tx.execute(query);
|
||||
rows = r.rows;
|
||||
});
|
||||
return { rows };
|
||||
const r = await db.execute(query);
|
||||
return { rows: r.rows };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -96,7 +94,6 @@ export async function pgGetFournisseurs(search?: string): Promise<{ code: string
|
||||
${search ? sql`AND (fi.nom ILIKE ${'%' + search + '%'} OR fi.code ILIKE ${'%' + search + '%'})` : sql``}
|
||||
ORDER BY fi.nom
|
||||
`);
|
||||
console.log(`[pg-ff] pgGetFournisseurs: ${result.rows.length} fournisseurs`);
|
||||
return (result.rows as unknown as { code: string; nom: string }[]).filter(r => r.code && r.nom);
|
||||
} catch (e) {
|
||||
console.error("[pg-ff] pgGetFournisseurs error:", (e as Error).message?.slice(0, 200));
|
||||
@@ -297,10 +294,6 @@ export async function pgGetGammesByFournisseur(codefou: string): Promise<Map<str
|
||||
for (const row of result.rows as unknown as { codein: string; gamme_code: string }[]) {
|
||||
if (row.codein && row.gamme_code) map.set(row.codein, String(row.gamme_code).trim());
|
||||
}
|
||||
if (result.rows.length > 0) {
|
||||
console.log("[pg-ff] Gammes sample row:", JSON.stringify(result.rows[0]));
|
||||
}
|
||||
console.log(`[pg-ff] Gammes: ${map.size} articles avec gamme`);
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -429,7 +422,6 @@ export async function pgGetNomenclatureByFournisseur(codefou: string): Promise<M
|
||||
}
|
||||
|
||||
console.log(`[pg-ff] Nomenclature: ${result.rows.length} articles, parentCol="${parentCol ?? "none"}"`);
|
||||
if (result.rows.length > 0) console.log("[pg-ff] Nomenclature sample:", JSON.stringify(result.rows[0]));
|
||||
return buildNomMap(result.rows as unknown as PgNomRow[]);
|
||||
}
|
||||
|
||||
@@ -635,9 +627,9 @@ export async function pgGetDashboardData(): Promise<DashboardData> {
|
||||
|
||||
// Deux appels parallèles : données hier + données même jour N-1
|
||||
const [apiResult, apiN1Result] = await Promise.all([
|
||||
fetch(`${apiBase}/dashboard?date=${dateHier}`, { cache: "no-store" })
|
||||
fetch(`${apiBase}/dashboard?date=${dateHier}`, ffFetchInit())
|
||||
.then(r => { if (!r.ok) throw new Error(`API dashboard ${r.status}: ${r.statusText}`); return r.json() as Promise<ApiPerfDashboard>; }),
|
||||
fetch(`${apiBase}/dashboard?date=${dateN1Param}`, { cache: "no-store" })
|
||||
fetch(`${apiBase}/dashboard?date=${dateN1Param}`, ffFetchInit())
|
||||
.then(r => r.ok ? r.json() as Promise<ApiPerfDashboard> : null)
|
||||
.catch(() => null),
|
||||
]);
|
||||
@@ -686,7 +678,7 @@ export async function pgGetDashboardData(): Promise<DashboardData> {
|
||||
const batch = allCodeins.slice(i, i + 5);
|
||||
await Promise.all(batch.map(async (codein) => {
|
||||
try {
|
||||
const artRes = await fetch(`${FF_API_BASE}/api/articles?codein=${encodeURIComponent(codein)}`);
|
||||
const artRes = await fetch(`${FF_API_BASE}/api/articles?codein=${encodeURIComponent(codein)}`, ffFetchInit());
|
||||
if (!artRes.ok) return;
|
||||
const artData = await artRes.json();
|
||||
if (!artData.articles || artData.articles.length === 0) return;
|
||||
@@ -695,8 +687,8 @@ export async function pgGetDashboardData(): Promise<DashboardData> {
|
||||
if (!noId) return;
|
||||
|
||||
const [refRes, dernierRes] = await Promise.all([
|
||||
fetch(`${FF_API_BASE}/api/articles/${encodeURIComponent(noId)}/referentiel`),
|
||||
fetch(`${FF_API_BASE}/api/articles/${encodeURIComponent(noId)}/dernier-fournisseur`),
|
||||
fetch(`${FF_API_BASE}/api/articles/${encodeURIComponent(noId)}/referentiel`, ffFetchInit()),
|
||||
fetch(`${FF_API_BASE}/api/articles/${encodeURIComponent(noId)}/dernier-fournisseur`, ffFetchInit()),
|
||||
]);
|
||||
|
||||
// Stock (toujours depuis referentiel)
|
||||
@@ -1373,7 +1365,7 @@ function mapCommandeAutoRow(r: any): PgCommandeAutoRow {
|
||||
export async function pgGetCommandesAuto(): Promise<PgCommandeAutoRow[]> {
|
||||
|
||||
try {
|
||||
const res = await fetch(`${FF_API_BASE}/api/commandes-auto`, { cache: "no-store" });
|
||||
const res = await fetch(`${FF_API_BASE}/api/commandes-auto`, ffFetchInit());
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
// L'API retourne { count: N, propositions: [...] } ou un tableau direct
|
||||
@@ -1414,7 +1406,7 @@ export async function pgGetCommandesAuto(): Promise<PgCommandeAutoRow[]> {
|
||||
uniqueCodeFous.map(async codefou => {
|
||||
// 1. Essai sans filtre site
|
||||
try {
|
||||
const res2 = await fetch(`${FF_API_BASE}/api/commandes-auto/${encodeURIComponent(codefou)}`, { cache: "no-store" });
|
||||
const res2 = await fetch(`${FF_API_BASE}/api/commandes-auto/${encodeURIComponent(codefou)}`, ffFetchInit());
|
||||
if (res2.ok) {
|
||||
const detail = await res2.json();
|
||||
const f = extractFranco(detail);
|
||||
@@ -1423,7 +1415,7 @@ export async function pgGetCommandesAuto(): Promise<PgCommandeAutoRow[]> {
|
||||
} catch { /* ignore */ }
|
||||
// 2. Fallback site=000
|
||||
try {
|
||||
const res3 = await fetch(`${FF_API_BASE}/api/commandes-auto/${encodeURIComponent(codefou)}?site=000`, { cache: "no-store" });
|
||||
const res3 = await fetch(`${FF_API_BASE}/api/commandes-auto/${encodeURIComponent(codefou)}?site=000`, ffFetchInit());
|
||||
if (res3.ok) {
|
||||
const detail = await res3.json();
|
||||
const f = extractFranco(detail);
|
||||
|
||||
Reference in new issue
Block a user