scripts/diagnostic-ff.js (lecture seule) relève les réglages PostgreSQL, la taille des tables, les types des colonnes de jointure, les index, les codes article avec espaces et les plans d'exécution de six requêtes représentatives (EXPLAIN, ou EXPLAIN ANALYZE avec --analyze). Il prépare les réécritures SQL qui demandent la vraie base. Mode d'emploi dans docs/plan-optimisation-webui.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AcA129WzGTH6zxPHFBGWmn
34 KiB
Plan — CollectFlow : interface simple, lisible et rapide
Contexte
Analyse des 13 pages (+ layout, menu, en-tête, API, accès BDD). Constats principaux :
- Lisibilité : plus de 160 textes entre 8,5 et 11 px. Libellés en MAJUSCULES grasses gris pâle, sous le seuil de contraste AA. Jargon partout (Init., PCB, PRMP, Franco, Sync Qlik, CODEIN…). Libellés mi-anglais, mi-français (Dashboard, Analytics, Snapshots).
- Incohérence :
- Thème : 5 pages sont figées en blanc (
bg-gray-50) dans une application sombre. Les classesdark:suivent le réglage de l'OS, pas le bouton de thème. - Composants : 7 styles d'en-tête de page, 13 tableaux codés à la main, formateurs de nombres copiés dans 12 fichiers.
- Vocabulaire : magasins nommés de 8 façons. « Réseau » désigne tantôt les ~270 magasins, tantôt nos 2.
- Thème : 5 pages sont figées en blanc (
- Ergonomie :
- La recherche de l'en-tête ne filtre que la Grille, même depuis les autres pages.
- L'utilisateur « Michael » est codé en dur dans l'en-tête.
- La Grille affiche deux boutons « Valider » et deux exports, avec des droits différents.
- Chaque chargement de page bloque tout l'écran (modale plein écran), et aucune page d'erreur n'existe.
- Lenteur :
- Hors tableau de bord, aucune donnée n'est mise en cache, alors que les données FF ne changent qu'une fois par nuit.
- Une Grille froide se reconstruit en 10 à 43 s. Ce calcul a lieu après chaque enregistrement, après chaque changement de magasin, et à chaque retour par le menu après un clic sur « Rafraîchir » : le paramètre
_refreshreste collé à l'URL. - La Grille rend tout le tableau à chaque paquet de 150 lignes, donc un coût quadratique.
exceljsetjspdfsont chargés pour tous les utilisateurs.- Plusieurs requêtes SQL ne peuvent pas utiliser d'index :
TRIM(codein),TO_CHAR(datmvt).
- Bugs relevés :
- Après un filtre, la sélection multiple de la Grille vise d'autres produits.
- Le filtre « passées » de Publicités ne s'applique pas (clé
passeesdans le filtre,passeedans les libellés). - L'erreur de chargement des Snapshots n'est jamais affichée.
- La carte KPI du tableau de bord répète sa valeur principale.
Choix validés : thème clair par défaut ; menu regroupé et renommé, avec Snapshots et Exports fusionnés en « Historique » ; livraison par lots, une PR par lot, sur la branche claude/keen-maxwell-hbtho6.
Conventions du dépôt :
- préfixe
rtksur toutes les commandes ; - commits en français ;
- plan de chaque lot rangé selon BMAD (Business / Model / API / Data,
.agents/rules/general.md).
Lot 1 — Vitesse et bugs, sans changement visuel (PR 1)
Risque faible, gain immédiat.
Grille (client)
src/features/grid/components/grid-client.tsx:- retirer
_refreshde la requête mémorisée dansactiveGridQuery(l.86-91), puis le supprimer de l'URL après usage avecrouter.replace; - sortir
code1etcode2des dépendances du chargement : le serveur ne les lit pas.
- retirer
grid-client.tsx:152-156: accumuler les paquets reçus et ne mettre à jour le store que toutes les ~250 ms, plus une fois à la fin, au lieu de reconstruire à chaque paquet.heatmap-grid.tsx:- ajouter
getRowId: r => r.codein(corrige le bug de sélection) ; - mémoriser
MONTHS_12sur sa clé texte (l.628) ; - retirer
transition-allsur les lignes et le flou du bandeau d'en-tête.
- ajouter
export-dropdown.tsx:exceljs,jspdfetjspdf-autotablepassent enawait import()dans les gestionnaires ;- le composant est chargé via
next/dynamic; - remplacer
useGridStore()sans sélecteur (l.12).
- Créer
src/lib/format.ts: instancesIntl.NumberFormatcréées une seule fois, plusfmtEur,fmtQte,fmtPct,fmtDate. Les cellules de la Grille l'utilisent ; les autres pages au lot 3.
Grille (serveur) — src/features/grid/api/get-product-rows.ts
- Lancer la requête des métriques Qlik et celle du dernier snapshot dans le même
Promise.allque la phase 1 (l.130-146, 410, 416). qlik-network-cache.ts: ne sélectionner que les colonnes utiles, sans le JSONBmetricsByMonth.- Cache mémoire :
- le borner (LRU d'environ 30 entrées) ;
- garder la déduplication des calculs en cours même en
forceRefresh; - transmettre
request.signaldepuissrc/app/api/grid/rows/route.ts.
- Après un enregistrement, mettre à jour en place le
codeGammedes lignes en cache au lieu d'invalider tout le cache (save-draft-changes.ts:73). Nouvelle fonctionpatchGridRowsCache()à côté deinvalidateGridRowsCache(). - Supprimer les
console.logdes chemins chauds :get-product-rows.ts(l.83, 123, 148, 433, 444, 622) etpg-ff-client.ts(l.99, 300-303, 431, 516).
Cache des données FF jusqu'à la synchro nocturne
- Créer
src/lib/ff-cache.ts: enveloppesunstable_cacheétiquetéesff-data, durée 6 h, sur le modèle déjà en place dansdashboard/page.tsx:4-8. Elles couvrent :- les 3 requêtes de stock ;
- le hit-parade (clé : dates) ;
- les CA par fournisseur et par nomenclature (clé : mois et mode) ;
pgGetFournisseurs;getFournisseursde la Grille ;- la dernière réception par fournisseur ;
pgGetCommandesAuto.
- Publicités :
fetchavecnext: { revalidate, tags }au lieu deno-store. - À la fin du cycle nocturne (
src/features/admin/api/sync-scheduler.ts) : invalider l'étiquetteff-data, puis préchauffer le tableau de bord et le stock. - Points à vérifier :
- la signature de
revalidateTagen Next 16 ; - que les valeurs mises en cache passent en JSON (les
Datedeviennent des chaînes).
- la signature de
Base de données et API
src/db/index.ts:- pool rangé dans
globalThis; options: "-c statement_timeout=60000 -c max_parallel_workers_per_gather=0"etapplication_name.pgNoParallel(pg-ff-client.ts:25-33) n'a alors plus besoin de BEGIN/SET/COMMIT : 3 allers-retours de moins par requête.
- pool rangé dans
AbortSignal.timeoutsur tous les appels à l'API FF :api-ff-client.ts,publicites/page.tsx, appels du tableau de bord.- Index des tables applicatives, dans
scripts/db-init.jsetsrc/db/schema.ts:session_snapshots(code_fournisseur, created_at DESC);session_snapshots(user_id, type, created_at DESC).
- Divers :
- Commandes auto : supprimer le double rechargement, en retirant
router.refresh()puisquerevalidatePathest déjà appelé (cadencier-client.tsx:172-177). - Synchronisation : PATCH envoyé à la sortie du champ, plus à chaque frappe (
admin/synchronisation/client.tsx:209-237). - Paramètres : un seul appel à
getSavedDatabaseConfig. log-capture.ts: sortie immédiate si aucune capture n'est ouverte.
- Commandes auto : supprimer le double rechargement, en retirant
Bugs fonctionnels
- Publicités : aligner la clé
passees/passee(publicites/client.tsx:141, 256). - Historique : afficher l'état d'erreur (
snapshot-list.tsx:133). - Tableau de bord : la carte KPI répète sa valeur (
dashboard/page.tsx:183-185). - Barre de résumé de la Grille :
filters.magasinn'est jamais renseigné, donc les snapshots enregistrent toujours « TOTAL » (floating-summary-bar.tsx:104).
Lot 2 — Socle visuel, menu et vocabulaire (PR 2)
Thème et typographie — src/app/globals.css, theme-provider.tsx
defaultTheme="light".- Ajouter
@custom-variant dark (&:where(.dark, .dark *));pour que les classesdark:suivent le bouton de thème. - Ajouter
@import "tw-animate-css";. - Faire correspondre les jetons shadcn (
--color-background,--color-muted-foreground,--color-accent…) aux variables de l'application. - Contraste AA (≥ 4,5:1) pour
--text-muteden clair et en sombre. - Échelle typographique :
- rien sous 12 px ;
- cellules de tableau à 13-14 px ;
- libellés en casse normale, sans MAJUSCULES
font-black tracking-widest.
- Une seule famille de boutons (fusion de
.apple-btn-*et.btn-action-*) ; définir ou retirer les classes inexistantes (apple-btn,--border-subtle,--surface-tertiary).
Composants partagés — dans src/components/ui/
PageHeader: titre, phrase d'explication, actions.Card/Section,StatCard(KPI avec écart N-1).Badge,DeltaBadge,StoreBadge,GammeBadge.Tabs, avec l'onglet actif dans l'URL.Select,SearchInput.EmptyState,ErrorState,Skeleton.ConfirmDialogsurui/dialog. Il remplacewindow.confirm,alert()et les 3 modales faites à la main.- Notifications discrètes (
sonner, standard shadcn) à la place deSuccessModalpour les succès. DataTable<T>: généralisé depuisStockTable<T>(stock-negatif/client.tsx:168-384), qui gère déjà colonnes, tri, recherche, pagination et export. Ajouter en-tête collant, état vide, et infobulle sur les en-têtes.
Sources uniques
src/lib/magasins.ts: 292 → « Frouard (Nancy) », 579 → « Houdemont », TOTAL → « Nos 2 magasins ». Il remplaceSITE_NAMES(dashboard),months.ts:91-94et les 3 copies deSITES.src/lib/glossaire.ts: libellé clair et infobulle pour chaque terme métier. Exemples :- Gamme A–Z avec son sens, à reprendre de
bulk-action-toolbar.tsx:8-12; - Réseau = environ
NB_MAGASINS_RESEAUmagasins (network-trend.ts:13) ; - Franco, PCB, PRMP, GTIN, N-1, Init.
- Gamme A–Z avec son sens, à reprendre de
- Règles de vocabulaire :
- « Réseau » = les ~270 magasins uniquement ; nos 2 magasins = « Nos magasins » ;
- Famille et Sous-famille identiques partout ;
- vouvoiement partout.
- Seuils de marge : une seule constante, au lieu de 25/15 % sur le tableau de bord et 40/25 % sur la fiche.
Menu — src/components/shared/sidebar.tsx
- Menu déplié par défaut, en groupes :
- Au quotidien : Accueil, Révision d'assortiment, Recherche produit, Stocks à surveiller, Commandes fournisseurs.
- Analyses : Meilleures ventes, Ventes par mois, Publicités.
- Historique : nouvelle page
/historiquequi fusionne Snapshots et Exports, avec un onglet par type ; l'onglet Exports est réservé aux admins./snapshotset/exportsy redirigent. - Administration : Synchronisation, Paramètres.
- Icônes distinctes pour chaque entrée.
src/middleware.ts: protéger aussi côté serveur la partie admin de l'historique.
En-tête et coquille
header.tsx:- retirer la recherche globale, qui ne filtrait que la Grille ; la Grille garde la sienne ;
- afficher le vrai utilisateur ;
- réglages personnels (thème, densité de la Grille) dans un menu utilisateur accessible à tous.
- Remplacer les 11
loading.tsx(modale plein écran) par des squelettes de page non bloquants. - Ajouter
app/(dashboard)/error.tsx(message clair et bouton « Réessayer ») etnot-found.tsx. - Titre de l'application en français (
app/layout.tsx).
Lot 3 — Migration des pages, une à une (PR 3, découpable en 3a/3b)
Règle commune à chaque page :
PageHeaderavec une phrase « à quoi sert cette page » ;DataTableet composants partagés, jetons du thème ;- retrait des enveloppes
min-h-screen bg-gray-50 p-6(double marge) ; - noms de magasins depuis
magasins.ts, infobulles depuisglossaire.ts; - états vide et erreur explicites.
| Page | Changements spécifiques |
|---|---|
Accueil (dashboard) |
Cartes StatCard. Section « Hit Parade » renommée « Top 10 d'hier ». Chaque produit mène à sa fiche. Couleurs codées en dur remplacées par les jetons. Message d'erreur compréhensible, sans trace technique. |
Révision d'assortiment (grid) |
Barre d'outils : un seul « Enregistrer » (dans la barre de résumé), un seul menu Export, un seul « Actualiser », avec les mêmes droits partout (isAdmin calculé côté serveur via auth() dans grid/page.tsx). Les actions rares (Non travaillés, Vues, Colonnes, Synchro Qlik) passent dans « Plus d'options ». Libellés : gammes affichées avec leur sens (« A — Cœur ») ; menu Colonnes avec de vrais libellés, pas les identifiants ; « / 270 » remplacé par NB_MAGASINS_RESEAU. Chargement : la superposition floue est remplacée par une barre de progression légère. Accueil fournisseur : une phrase-guide. |
Recherche produit (produits) |
Tailles ≥ 12 px dans fiche.tsx, results.tsx et opportunites.tsx. Infobulles PCB, PRMP, GTIN. Famille et Sous-famille alignées. Message de chargement sans « Qlik Sense ». |
Meilleures ventes (hit-parade) |
DataTable paginé ; état vide ; « % Marge » triable. Export via exceljs en import dynamique, ce qui permet de retirer xlsx. |
Stocks à surveiller (stock-negatif) |
Base du DataTable. En-têtes Excel lisibles (« Code article » au lieu de CODEIN, etc.). Noms de magasins. |
| Publicités | Le tri et le filtre magasin s'appliquent à l'ensemble des données, pas seulement à la page de 50. Infobulles « Taux de sortie » et « % CA total ». |
Commandes fournisseurs (commandes-auto) |
Onglets dans l'URL. Confirmation avant suppression. Erreurs affichées en notification. Retour visuel à l'enregistrement d'une fréquence. Infobulle « Franco ». |
Ventes par mois (analytics) |
Dernier mois complet par défaut. Mois en clair (« oct. 2026 vs oct. 2025 »). Recherche, tri, export, état vide. |
| Historique | Page fusionnée (lot 2), libellés de session lisibles. |
| Paramètres | Onglets Connexions / Utilisateurs / Clés API / Journal. Un bouton « Enregistrer » par section (le bouton collant trompeur disparaît). Thème et densité déplacés dans le menu utilisateur. |
| Synchronisation | Unités affichées (« jours », « heures »). Filtres en casse normale. Libellés sans jargon (« Tour bouclé » → « Tous les fournisseurs traités »). |
| Connexion | Libellés lisibles. Slogan « Propulsée par l'IA » retiré. Rendu clair. |
Lot 4 — Optimisations lourdes (PR 4)
Grille servie depuis l'instantané grid_rows (le plus gros gain)
- La table est déjà remplie chaque nuit avec exactement les
ProductRowdu magasin TOTAL :persistGridSnapshotdansget-product-rows.ts, etsync-scheduler.ts:173. - Ajouter
readGridRowsPayload(codeFournisseur)danssrc/lib/grid-store.ts, qui litpayload::textet l'envoie tel quel, sans analyse ni réencodage. - Superposer ensuite la gamme INIT à jour (
refreshGammeInit) et les gammes du dernier snapshot. - Si l'instantané est trop ancien, relancer le calcul en arrière-plan (stale-while-revalidate).
- Ouverture à froid : de 10-43 s à environ 1-2 s.
Changement de magasin instantané
- Toujours charger TOTAL : les lignes portent déjà le détail par magasin.
- Bascule côté client avec
setActiveMagasinethistory.replaceState, sans aller-retour serveur (grid-filter-bar.tsx:100-105). - Le rattrapage via l'API FF (
reconcileSelectedStoreFromMensuelApi) devient une petite requête séparée,/api/grid/rows/store-patch, appliquée quand elle arrive.
Rendu de la Grille
React.memosurHeatmapGrid,GridFilterBaretFloatingSummaryBar.useSaveDraftsdéplacé dans le bouton Enregistrer.- En-tête de tableau mémorisé ;
useFlushSync: falsesur le virtualiseur. - Largeurs de colonnes en variables CSS.
persistde zustand sans les lignes, et limité en fréquence.- Tendance réseau et texte de recherche calculés une seule fois par ligne à la réception ; le filtre global ne porte plus que sur ce texte.
- Lignes gardées en mémoire au retour sur la Grille (même fournisseur, moins de 10 min) : pas de
setRows([]).
SQL sans index utilisable
À valider par EXPLAIN ANALYZE sur la base réelle avant fusion : le schéma des tables FF n'est pas dans le dépôt.
- Fiche produit (
get-produit-fiche.ts,pg-ff-client.ts:1737-2045) : résoudreno_idune fois, puis filtrer sur les clés ; lecture du cache Qlik dans le mêmePromise.all; supprimer la double recherche du chemin?cc=. - Ventes par mois (
pg-ff-client.ts:827-885) : plages de dates au lieu deTO_CHAR. - Stocks : réécrire
pgGetStockSansVente(l.1198-1235) en agrégeant d'abord par (article, site). Charger seulement l'onglet actif. - Meilleures ventes (l.922-991) : jointure sur
artnoid/no_id. - Commandes : la dernière réception est limitée aux fournisseurs du cadencier et à une fenêtre de dates ; appels « franco » à concurrence bornée, avec délai maximal ; un
Suspensepar onglet. - Tableau de bord (l.623-797) : une seule requête SQL sur
cube_stock, en réutilisantpgGetStockForCodeins, au lieu de ~60 appels HTTP. Cache par date, préchauffé après la synchro. /api/v1/grid: pagination par curseur (keyset) au lieu d'OFFSET ; COUNT une seule fois.api-auth.ts:last_used_atmis à jour au plus une fois par minute.
Nettoyage
- Dépendances inutilisées :
@tanstack/react-query,@google/genai,ai,react-markdown, etxlsxaprès le lot 3. - Code mort :
supplier-selection.tsx,financial-cell.tsx,ui/textarea.tsx,getGridDataetgetAvailableNomenclature(grid/actions.ts).
Hors périmètre, à signaler
src/features/settings/actions.ts ne vérifie aucun droit. Tout utilisateur connecté peut lire l'URL de la base, le mot de passe Qlik et les clés IA, et réécrire la configuration. Ce point est proposé en tâche séparée.
Vérification (à chaque lot)
- Contrôles automatiques :
rtk tsc: aucune nouvelle erreur par rapport àtsc_errors.log, carignoreBuildErrorsest activé.rtk lint.rtk next build: comparer le JS de premier chargement de/gridavant et après (lot 1 : −exceljs/jspdf).
- Visuel :
rtk npm run dev, puis Playwright (Chromium préinstallé) pour capturer chaque page en clair et en sombre, en 1920 et 1366 px de large.- Un script dans la page signale tout texte calculé sous 12 px et tout élément
bg-whiteen mode sombre.
- Vitesse :
- mesurer le temps de réponse de
/api/grid/rows(froid, chaud, après enregistrement), et de/stock-negatifet/hit-paradeau 1er puis au 2e chargement ; - objectif : 2e visite en moins de 200 ms, Grille froide en moins de 2 s au lot 4.
- mesurer le temps de réponse de
- Recette manuelle sur l'environnement réel (ce conteneur n'a pas accès à PostgreSQL FF, à l'API FF ni à Qlik) :
- gros fournisseur ;
- changement de magasin instantané ;
- enregistrer puis rouvrir sans attente ;
- sélection multiple après filtre, sur les bons produits ;
- après « Actualiser », un retour par le menu ne force plus le recalcul ;
- exports Excel identiques.
Suivi — Lot 1 réalisé
Écarts par rapport au plan, décidés pendant l'implémentation :
-
Cache FF : un cache mémoire dédié (
src/lib/ff-cache.ts) remplaceunstable_cache. Deux raisons :unstable_cachesérialise en JSON, ce qui casse lesMapet lesDate;revalidateTagne peut pas être appelé depuis la synchro nocturne, qui tourne hors requête.
Fonctionnement :
- durée de vie de 30 min (
FF_CACHE_TTL_S), et 10 min pour les publicités ; - les appels simultanés partagent une seule requête ;
- les erreurs ne sont jamais gardées ;
- le cache est vidé en fin de synchro nocturne.
-
statement_timeout: activé seulement siPG_STATEMENT_TIMEOUT_MSest défini, pour ne pas couper les longs calculs nocturnes. -
Colonnes Qlik : non modifié.
metricsByMonthsert à la courbe « nombre de magasins vendeurs ». -
Carte KPI du tableau de bord : non modifiée. La ligne « Hier / N-1 » est une comparaison voulue, pas un doublon.
-
Préchauffage après la synchro : reporté. L'invalidation suffit, la première visite recharge.
Suivi — Lot 2 réalisé
- Thème :
- clair par défaut ; les classes
dark:suivent le bouton de thème ; - animations (
tw-animate-css) réellement importées ; - couleurs shadcn branchées sur les variables ;
- libellés gris au contraste AA dans les deux thèmes ;
- fond plein pour le menu et l'en-tête (le flou ne servait à rien).
- clair par défaut ; les classes
- Composants partagés, dans
src/components/ui/:PageHeader,Card,Field,StatCard;Badge,DeltaBadge,StoreBadge,GammeBadge;Tabs,Segmented,useUrlTab;Select,SearchInput,Input;EmptyState,ErrorState,Skeleton,PageSkeleton;Tooltip,Terme(glossaire) ;toastetconfirmer(feedback.tsx) ;Pagination,DataTable.
- Sources uniques :
lib/magasins.ts(noms des magasins) ;lib/gammes.ts(A à Z, avec leur sens et leurs couleurs) ;lib/glossaire.ts;lib/marge.ts(seuils 40 / 25 %, ceux de la Grille) ;lib/export-excel.ts(export unique, via exceljs).
- Navigation :
- menu groupé (Au quotidien / Analyses / Suivi / Administration), déplié par défaut, libellés en français ;
- en-tête : nom de la page, thème, menu du compte (vrai nom, hauteur des lignes de la Grille pour tous, déconnexion) ;
- la recherche globale, qui ne filtrait que la Grille, est retirée.
- Historique : nouvelle page
/historique(sessions et exports), et redirection des anciennes adresses/snapshotset/exports. Les deux onglets sont visibles par tous : chacun n'y voit que ses propres enregistrements. - Chargements et erreurs :
- squelettes de page au lieu de la fenêtre plein écran ;
- pages d'erreur (
error.tsx) et « page introuvable » en français ; - après connexion, arrivée sur l'Accueil, comme pour
/.
- Écart : les en-têtes Excel de la page Stocks (CODEIN, GENCODE, QTE, CODMV, avec les codes 503 / 412) ne seront pas renommés. C'est vraisemblablement un format d'import de régularisation dans FF.
Suivi — Lot 3 réalisé
Toutes les pages utilisent désormais :
PageHeaderavec une phrase d'explication ;- les couleurs du thème (plus de pages figées en blanc) ;
- des textes d'au moins 12 px ;
- les noms de magasins,
toastetconfirmer.
Vérifié dans un navigateur : sur les pages visitées, en clair et en sombre, aucun texte n'est sous 12 px.
| Page | Changements principaux |
|---|---|
| Accueil | Cartes « Nos 2 magasins » : CA, tickets et panier moyen, avec l'évolution N-1. Une carte par magasin. Top 10 lisible, avec un lien vers la fiche produit. |
| Révision d'assortiment | Barre du bas : seul endroit pour enregistrer et exporter, avec les mêmes droits pour tous. Le menu Export regroupe les deux anciens. Bouton « Annuler » avec confirmation. Filtres : sens des gammes affiché (A Cœur…), magasin choisi en un clic, menu « Colonnes » en clair. Chargement : bandeau de progression au lieu de la fenêtre floue. Droits : lus côté serveur. |
| Recherche produit | Fiche lisible (Secteur / Rayon / Famille), infobulles PCB, PRMP, code-barres, réseau. Plus de mention technique de Qlik. |
| Stocks à surveiller | DataTable avec trois onglets et une explication pour chacun. Export Excel inchangé (format d'import FF). |
| Meilleures ventes | DataTable avec un tri sur toutes les colonnes et une ligne de totaux. Export via exceljs (xlsx n'est plus utilisé). |
| Ventes par mois | Dernier mois complet par défaut, mois en clair, bascule Fournisseur / Famille, recherche, tri, export. |
| Publicités | Onglets, statuts lisibles, part du CA calculée en pied de tableau. Le tri ne porte que sur la page affichée, et la page le signale. |
| Commandes fournisseurs | Onglets dans l'URL, confirmation avant suppression, retours de toutes les actions. |
| Historique | « Reprendre dans la Grille », chiffres visibles, états vides et d'erreur. |
| Paramètres | Onglets Connexions / Utilisateurs / Accès API / Journal serveur, un bouton Enregistrer par section. |
| Synchronisation | Vocabulaire clair, unités affichées, résumé en indicateurs. |
| Connexion | Libellés lisibles, fond sobre. |
Autres changements :
- Composants retirés :
SuccessModal,ConfirmModal,ChangePasswordModal,LoadingModal,export-dropdown,financial-celletsupplier-selection, qui n'étaient plus utilisés. - Correction : le nom de l'utilisateur est maintenant transmis à la session, et l'en-tête l'affiche.
Suivi — Lot 4 réalisé
Grille servie depuis l'instantané grid_rows
getProductRowslit d'abord l'instantané du fournisseur (readGridSnapshot), puis y reporte :- la gamme FF du moment (INIT) ;
- les gammes du dernier enregistrement.
- Hors « Nos 2 magasins », le rattrapage du magasin choisi est appliqué comme après un calcul complet.
- L'instantané n'est pas utilisé dans trois cas, et la Grille est alors recalculée comme avant :
- il est absent ;
- il date d'un autre mois (la fenêtre des 12 mois a changé) ;
- il est d'un ancien format. Un numéro de version (
payloadVersion) est désormais écrit dans chaque ligne, donc les instantanés existants seront recalculés une fois, à la première ouverture ou par la synchro de la nuit.
- Instantané de plus de 20 h : il est servi tout de suite, et un recalcul part en arrière-plan.
- « Actualiser » force toujours le calcul complet.
- Mesure locale (30 lignes) : 43 ms depuis l'instantané, 1 ms depuis le cache mémoire.
Grille côté navigateur
- Retour sur la Grille (même fournisseur et même magasin, moins de 10 min) : les lignes en mémoire sont réutilisées, sans nouveau téléchargement.
- Changement de magasin ou « Actualiser » sur le même fournisseur : la Grille reste affichée et utilisable. Les chiffres complétés remplacent l'affichage d'un bloc, une fois reçus.
React.memosur la Grille, la barre de filtres et la barre du bas. En-tête du tableau mémorisé. Virtualiseur sansflushSync.
Requêtes
- Fiche produit : les sept lectures, y compris le réseau Qlik, partent en même temps au lieu de deux vagues successives.
- Ventes par mois : filtre sur des plages de dates au lieu de
TO_CHAR(datmvt), ce qui permet à PostgreSQL d'utiliser un index surdatmvt. Résultats identiques vérifiés sur une base de test (colonnesdateettimestamp, bornes de mois). - Stock sans vente : même principe pour la borne « avant le mois en cours ».
- Accueil :
- enrichissement des produits par vagues de 12 appels au lieu de 5 ;
- cache porté à 30 min, puisque ce sont les ventes de la veille.
- API
/api/v1:- le comptage et la page sont lus en parallèle ;
last_used_atd'une clé est écrit au plus une fois par minute ;- la version interne des lignes n'est pas exposée.
Non fait volontairement (à reprendre avec un accès à la base réelle)
- Réécritures SQL lourdes : Meilleures ventes par
artnoid, Stock sans vente agrégé d'abord, tableau de bord en une requête SQL au lieu des appels HTTP. Sans le schéma ni les volumes de la base FF, impossible de vérifier l'équivalence des résultats et le gain parEXPLAIN ANALYZE. - Index sur les tables FF (
mouvements.datmvt,codein) : ces tables ne sont pas gérées par CollectFlow. À proposer à l'administrateur de la base aprèsEXPLAIN ANALYZE. - Pagination par curseur de
/api/v1/grid: elle changerait le contrat public (page) utilisé par des appelants externes. - Suppression des dépendances inutilisées (
@tanstack/react-query,@google/genai,ai,react-markdown,xlsx) : le dépôt a deux fichiers de verrouillage (npm et pnpm). À faire avec le gestionnaire de paquets réellement utilisé en production.
À vérifier en production
- Première ouverture d'un gros fournisseur après la synchro de la nuit : elle doit être quasi immédiate.
- Changement de magasin : la Grille reste affichée, puis les chiffres se mettent à jour.
- Enregistrer, quitter la Grille, puis y revenir : les gammes enregistrées sont bien là.
Suivi — Lot 5 réalisé (correctif Grille et sécurité)
Grille : changement de magasin
- Corrige une régression du lot 4 : les lignes déjà affichées gardaient les chiffres de l'ancien magasin, et un tri actif n'était pas réappliqué, jusqu'à la fin du rechargement. Désormais, la liste est reconstruite à chaque changement de magasin : les chiffres et le tri sont justes tout de suite, et la sélection est conservée.
- Plus d'aller-retour serveur : la Grille charge toujours « Nos 2 magasins », dont les lignes portent déjà le détail de chaque magasin. Changer de magasin met seulement à jour l'URL, qui reste partageable.
- Complément de l'API FF demandé à part (
/api/grid/rows/store-patch) et appliqué à son arrivée. Il couvre les articles sans vente SQL dans le magasin, comme avant.- Le calcul est identique à l'ancien : comparaison automatique sur un jeu d'essai.
- Il ne modifie que les chiffres du magasin, pas les totaux « Nos 2 magasins ».
- Il n'est demandé qu'une fois par magasin et par chargement.
- Sélection : remise à zéro seulement au changement de fournisseur.
- La recherche ne reconstruit plus toutes les lignes à chaque frappe.
Sécurité
- Contrôle commun
lib/authz.ts:- le compte est relu en base, avec un cache d'une minute ; un compte supprimé ou rétrogradé perd ses droits sans attendre l'expiration de sa session ;
- si la base est injoignable, le jeton de session fait foi.
- Paramètres : actions réservées aux administrateurs, avec la même vérification sur les pages Paramètres et Synchronisation.
- Toutes les autres actions et routes vérifient la session : Grille, Commandes, Historique, exports, statut FF.
- Routes de diagnostic (
/api/diag*,/api/test-db) : réservées aux administrateurs, sans trace d'erreur technique. - Mots de passe et clés d'IA :
- ils ne sont plus envoyés au navigateur ; un champ laissé vide garde le mot de passe enregistré ;
- le mot de passe PostgreSQL n'est plus conservé dans le
localStorage, et l'ancien est effacé ; - enregistrer la connexion PostgreSQL n'efface plus les clés d'IA.
- Compte admin/admin :
- il n'est créé qu'au premier démarrage (aucun utilisateur en base) ;
- le fichier de secours
data/users.jsonne sert que si la base est injoignable ; data/est ignoré par git.
À savoir après la mise en ligne
- Un utilisateur promu administrateur doit se reconnecter pour accéder à Paramètres.
- Après avoir configuré la base depuis le compte de secours, il faut se reconnecter.
- Un mot de passe enregistré ne peut plus être effacé depuis l'écran ; seul son remplacement est possible.
Suivi — Lot 6 réalisé (performances restantes et nettoyage)
Grille
- Écriture des réglages dans le navigateur : au plus une par demi-seconde, et immédiate quand la page est quittée. Avant, chaque changement d'état en déclenchait une. Mesure : 8 changements de gamme donnent 1 écriture au lieu de 16.
- Calculs faits une seule fois par ligne : la tendance réseau (au lieu d'un calcul par case et par rendu) et le texte de recherche (au lieu d'un calcul par ligne et par frappe). La petite courbe de tendance est mémorisée.
- Barre du bas : elle ne suit plus que le nombre de modifications ; le détail est lu au moment d'enregistrer.
Commandes fournisseurs
- Montants de franco : demandés à l'API FF 6 fournisseurs à la fois, au lieu de tous en même temps.
- Dernière réception : calculée pour les seuls fournisseurs du cadencier. Sur la base de test, les résultats sont identiques au calcul complet.
Nettoyage
- Dépendances retirées :
@tanstack/react-query,@google/genai,ai,react-markdown,remark-gfm,xlsx,@radix-ui/react-dropdown-menu. Unnpm cicomplet a été vérifié. pnpm-lock.yamlsupprimé : Docker et le README utilisent npm.- Code mort retiré :
ui/textarea,debug/api/init-db,score/store;getGridDataetgetAvailableNomenclature;- 5 fonctions de l'ancien client HTTP FF,
pgGetArticlesByCodeCentraleetlibelleGamme.
- Journaux à la racine supprimés :
build.log,build_logs.txt,error.txt,tsc_errors.log.
Non fait volontairement
- Enrichissement de l'Accueil en SQL :
- le cache de la page sert la version précédente pendant son recalcul, donc le gain serait faible ;
- et rien ne garantit que la base FF relie chaque mouvement à son fournisseur (colonne détectée au cas par cas) : le fournisseur affiché pourrait changer.
- À reprendre après le diagnostic du lot 7.
- Largeurs de colonnes en variables CSS : peu de gain maintenant que l'écriture des réglages est différée.
Suivi — Lot 7 réalisé (diagnostic de la base FF)
Les réécritures SQL restantes demandent de connaître la vraie base FF : types des colonnes, index, volumes. Ce dépôt ne les contient pas. Elles concernent :
TRIM(a.codein::text)dans une quinzaine de requêtes ;TO_CHAR(datmvt)dans les regroupements ;- la jointure par code plutôt que par identifiant dans Meilleures ventes.
Le script scripts/diagnostic-ff.js les relève, en lecture seule :
- version et réglages de PostgreSQL ;
- taille des tables et date de dernière analyse ;
- types des colonnes de jointure et de filtre ;
- index existants ;
- nombre de codes article contenant des espaces (pour savoir si
TRIM()est utile) ; - plans d'exécution de six requêtes représentatives : recherche d'un article par code avec et sans
TRIM, Meilleures ventes sur 7 jours, dernière réception, ventes mensuelles d'un fournisseur, stock sans vente.
À lancer sur le serveur de production (conteneur web du docker-compose) :
docker compose exec web node scripts/diagnostic-ff.js > diagnostic-ff.txt
# Avec les durées réelles (exécute les requêtes, à faire hors heures d'ouverture) :
docker compose exec web node scripts/diagnostic-ff.js --analyze > diagnostic-ff-analyze.txt
Le fichier produit ne contient aucun mot de passe, seulement des noms de tables, de colonnes et d'index, et des plans d'exécution. Une fois transmis, il permet de décider :
- quels
TRIM()retirer ; - quels index proposer à l'administrateur de la base FF ;
- s'il faut remplacer l'enrichissement HTTP de l'Accueil.