- API /api/v1 : PUT /products/:codein/gamme et POST /gammes déposent une
proposition (table gammes_a_valider) au lieu d'enregistrer. La Grille la
charge comme modification non enregistrée (badge « API ») : Enregistrer
la valide, Annuler la rejette. Lecture : champ gammeAValider, statut
a_valider dans la réponse ; OpenAPI mise à jour.
- Meilleures ventes, Ventes par mois, Publicités : plus de largeur max
centrée, les pages occupent toute la largeur.
- Meilleures ventes : colonne Référence (référence fournisseur), recherche
et export Excel compris.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaniLFvegAZNXFrFFSStqa
- Grille : réglages écrits dans le localStorage au plus toutes les 500 ms
(immédiatement à la fermeture), tendance réseau et texte de recherche
calculés une fois par ligne, courbe de tendance mémorisée, barre du bas
abonnée au seul nombre de modifications.
- Commandes : appels « franco » 6 par 6 ; dernière réception limitée aux
fournisseurs du cadencier (résultats identiques sur la base de test).
- Dépendances inutilisées retirées (react-query, @google/genai, ai,
react-markdown, remark-gfm, xlsx, @radix-ui/react-dropdown-menu) ;
pnpm-lock.yaml supprimé (npm est utilisé par Docker et le README).
- Code mort et journaux de build retirés.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AcA129WzGTH6zxPHFBGWmn
Grille
- Corrige le lot 4 : au changement de magasin, les chiffres et le tri
affichés suivent tout de suite (nouvelle liste), la sélection est gardée.
- Lignes toujours chargées « Nos 2 magasins » ; changement de magasin sans
aller-retour serveur (URL mise à jour par l'historique du navigateur).
- Complément de l'API FF par magasin servi à part (/api/grid/rows/store-patch),
appliqué à son arrivée, calcul identique à l'ancien (lib/store-patch.ts).
- La recherche ne reconstruit plus toutes les lignes à chaque frappe ; la
sélection n'est remise à zéro qu'au changement de fournisseur.
Sécurité
- lib/authz.ts : session relue en base (cache 1 min), rôle de la base
prioritaire ; variantes pour actions, routes et pages.
- Paramètres réservés aux administrateurs ; session vérifiée dans les actions
Grille, Commandes, Historique et les routes ; diagnostics réservés admin.
- Mots de passe et clés d'IA plus envoyés au navigateur ; champ vide = mot de
passe conservé ; plus de mot de passe dans le localStorage ; l'enregistrement
de la base n'efface plus les clés d'IA.
- admin/admin créé seulement si aucun utilisateur ; fichier de secours utilisé
uniquement si la base est injoignable ; data/ ignoré par git.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AcA129WzGTH6zxPHFBGWmn
- Grille : lecture de l'instantané grid_rows (gamme INIT et dernières gammes
enregistrées reportées), recalcul en arrière-plan au-delà de 20 h, version de
format dans chaque ligne pour écarter les anciens instantanés.
- Navigateur : lignes réutilisées au retour sur la Grille (< 10 min), Grille
conservée pendant un changement de magasin ou une actualisation, React.memo
sur la Grille, les filtres et la barre du bas, en-tête mémorisé.
- Fiche produit : toutes les lectures en parallèle.
- Ventes par mois et stock sans vente : plages de dates au lieu de TO_CHAR.
- Accueil : vagues de 12 appels, cache de 30 min.
- API v1 : comptage et page en parallèle, last_used_at au plus une fois par
minute, version interne non exposée.
- Plan : suivi du lot 4, avec ce qui reste à valider sur la base réelle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AcA129WzGTH6zxPHFBGWmn
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
Deux colonnes s'ajoutent à la suite de la tendance réseau :
· PV moyen (Réseau) — calculé par l'application, CA réseau ÷ quantité
vendue, exactement la définition déjà retenue par la fiche produit.
Sans quantité sur la période, la colonne affiche « - » : le rapport
n'existe pas, et un prix à 0 € se lirait comme un article donné.
· PV central (Article) — le prix de vente de la fiche article, lu en base
(article_infosup.prix_vente_mini), sans retraitement.
Les deux restent deux colonnes. En dériver un écart chiffré supposerait une
assiette commune — HT/TTC, remises — que les deux sources ne garantissent
pas ; le rapprochement se fait à l'œil, en connaissance de cause.
Le bouton de repli du bloc mensuel devient un menu « Vues » à deux entrées,
ventes mensuelles et prix, chacune repliable. Il agit par BLOCS, là où le
menu « Colonnes » voisin agit colonne par colonne : douze cellules
mensuelles ou deux prix ne se masquent pas une par une. Les deux états sont
persistés.
Au passage, le tri des colonnes réseau était faux. La sentinelle ±Infinity
choisie d'après le sens de tri courant ne pouvait pas fonctionner : TanStack
mémorise le résultat de l'accesseur dans `row._valuesCache` et ne le
réévalue jamais, si bien que la valeur calculée au premier tri restait figée
et remontait les lignes sans donnée en tête dès qu'on inversait le sens.
`sortUndefined: "last"`, traité avant l'inversion, les garde en bas dans les
deux sens. Les cinq colonnes réseau existantes en bénéficient, et `sorting`
quitte les dépendances des colonnes : plus de reconstruction complète à
chaque clic d'en-tête.
Vérifié sur build de production : les deux colonnes se placent bien après la
tendance, « - » sur quantité réseau nulle comme sur PV central absent, et le
tri place les absents en bas au premier clic (décroissant) comme au deuxième
(croissant). Menu « Vues » : 31 colonnes avec les deux blocs, 29 sans les
prix, 17 sans rien, 19 avec les prix seuls — chaque fois sur toutes les
lignes montées, sans rechargement, et l'état survit au rechargement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYGgoQCG1VCe42HzbzhAXR
La colonne « Tot. 12m » dit combien, jamais comment. Un clic sur la case
ouvre désormais le détail des 12 mois glissants, empilé dans l'ordre où on
le lit : les ventes mensuelles, puis les entrées en stock, puis le stock de
fin de mois magasin par magasin — un stock de 40 réparti sur deux sites et
un stock de 40 bloqué sur un seul ne se pilotent pas pareil.
Les deux premières séries suivent le magasin actif de la Grille, par
cohérence avec la case cliquée ; la ventilation par magasin reste entière,
c'est son objet. Les barres se dessinent de part et d'autre d'un zéro
explicite : une quantité mensuelle peut être négative (retours supérieurs
aux ventes) et sortirait sinon du cadre par le bas.
Le bloc des douze colonnes mensuelles se replie d'un bouton. L'état est un
drapeau à part, et non douze entrées de `columnVisibility` : masquer douze
colonnes une par une n'est pas une manipulation, et les identifiants
`month_YYYYMM` changent à chaque nouveau mois alors que le drapeau, lui,
survit à la fenêtre glissante. Les colonnes sont retirées du modèle plutôt
que cachées — inutile de faire calculer douze cellules par ligne à TanStack
pour ne rien peindre.
`TuileStat` sort de heatmap-grid.tsx vers un module partagé, la modale de
tendance réseau et la nouvelle modale s'en servant toutes deux.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYGgoQCG1VCe42HzbzhAXR
Trois défauts distincts, dont un que je venais d'introduire.
1. RÉGRESSION — grille vide. Le store zustand est persisté dans le navigateur
sans version ni migration. Le passage de filters.code3 à `string[] | null`
laissait donc une CHAÎNE dans le localStorage des utilisateurs :
`new Set("320211")` produit un ensemble de caractères, plus aucune ligne ne
correspond, et la Grille apparaît vide sans le moindre message. Ajout de
`version: 1` + `migrate`, plus une garde dans le filtre pour ne jamais
redevenir muet sur un état inattendu.
2. STOCK « tous magasins » incomplet. Le stock est un NIVEAU, pas un flux : un
magasin sans mouvement dans le mois détient toujours sa marchandise. Or le
TOTAL était sommé depuis les lignes mensuelles brutes, donc n'incluait que
les sites ayant bougé ce mois-là — sur un produit à faible rotation, il
n'affichait que Frouard. Il est désormais recalculé depuis les séries par
site, qui sont reportées d'un mois sur l'autre.
3. CHANGEMENT DE MAGASIN qui se fige. Hors « tous magasins », un rattrapage
interroge l'API FF à raison d'UNE requête HTTP par article : sur un gros
fournisseur, cela fait des milliers d'appels. Borné à 300 (réglable via
GRID_STORE_RECONCILE_MAX), avec un avertissement explicite sur ce qui n'a
pas été rattrapé — pas de troncature silencieuse.
Enfin, getProductRows ne renvoie plus [] en cas d'erreur : une liste vide est
indiscernable d'un fournisseur sans article. La Grille affichait une page
blanche sans explication, et la synchro nocturne prenait la panne pour un
fournisseur vide — qu'elle désactivait automatiquement. L'erreur remonte
désormais jusqu'au bandeau rouge et au statut « echec ».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
Le filtre était un <select> à valeur unique : impossible de retenir plusieurs
nomenclatures, ni de partir de tout pour retirer les postes non voulus — ce qui
est la manière naturelle de travailler quand un fournisseur en compte des
dizaines.
Remplacé par une liste déroulante à cases, avec recherche, « Tout cocher »,
« Tout décocher », « Ne garder que ces N » sur le résultat d'une recherche, et
une croix pour tout réafficher. Décocher depuis l'état « tout » matérialise la
liste complète puis en retire le poste, sinon le premier décochage n'aurait
rien fait.
filters.code3 passe de `string | null` à `string[] | null` : `null` = aucun
filtre, `[]` = rien de coché. Distinguer les deux évite la valeur sentinelle
qu'imposerait un simple tableau, et rend « Tout décocher » sans ambiguïté.
L'URL accepte une liste séparée par des virgules.
Au passage : la Grille rechargeait tout depuis le serveur à chaque changement
de nomenclature, alors que getProductRows ignore ce filtre et que le tri se
fait en local. Le paramètre n'est plus transmis ni dans les dépendances de
l'effet — le filtrage est désormais instantané, ce qui compte d'autant plus
avec plusieurs cases à cocher.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
Remove infinite grid loading, useInfiniteGrid hook, and server-side
pagination API route. Revert grid components to the simpler version
that avoids 2MB+ cache failures from unstable_cache.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>