mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
c53238638533cdf5081912118d4fcbe5d7d828aa
568
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c532386385 |
Grille : repeindre les lignes montées quand le jeu de colonnes change
Replier le bloc mensuel ne vidait que l'en-tête. Les lignes déjà montées gardaient leurs douze cellules, seules celles virtualisées ensuite adoptaient le nouveau jeu — une grille à deux vitesses, en-tête d'un côté, corps de l'autre. TanStack met ses objets `Row` en cache sur la seule identité des DONNÉES : changer les colonnes ne leur donne aucune référence neuve, et `React.memo` concluait qu'il n'y avait rien à repeindre. Le contournement existait déjà pour `columnVisibility` et `columnSizing`, passés en props sans jamais être lus ; le drapeau du bloc mensuel, lui, n'y figurait pas. `columnVisibility` cède la place à `columnsKey`, la signature des colonnes réellement visibles : elle change pour TOUT changement de colonnes, case décochée dans le menu comme bloc replié, là où l'ancien objet ne voyait que le menu. Calculée une fois par rendu de grille, pas une fois par ligne. Vérifié en rendant la Grille sur données de démonstration : 29 cellules par ligne et 29 en-têtes, 17 et 17 une fois le bloc replié, 29 et 29 au retour. Avec le signal figé, l'en-tête tombe à 17 quand les lignes restent à 29 — le défaut signalé. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYGgoQCG1VCe42HzbzhAXR |
||
|
|
2ae523a804 |
Grille : détail mensuel au clic sur le total, bloc mensuel repliable
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 |
||
|
|
92769535dc |
style(grille): agrandit la carte de tendance, illisible en l'état
Les textes du graphique tournaient entre 8,5 et 10 px : titres de séries, « max N », valeurs annotées, mois de l'axe, infobulle, ainsi que les légendes des trois tuiles. Difficile à lire. Typographie relevée d'un cran partout : titres de séries 9,5 → 13, maximum 9 → 11,5, valeurs sur les points 9 → 12,5, mois 8,5 → 11, infobulle 9 → 11,5, tuiles 10 → 12 avec la valeur à 26 px, tableau 11 → 13. Le dessin suit la typographie plutôt que de la subir : bandeau de facette 15 → 21, axe 18 → 24, écart 12 → 14, infobulle élargie à 196 avec un interligne de 17 — sans quoi titres et courbes se seraient chevauchés. La carte passe à max-w-2xl pour absorber le tout sans comprimer le graphique. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
a4e3c53dcc |
feat(grille): détail par magasin dans la carte, en mode tous magasins
En « tous magasins », les trois tuiles de la carte sont des cumuls : savoir que le stock est de 40 ne dit pas s'il est réparti entre Frouard et Houdemont ou entièrement sur un seul site — or c'est précisément ce qu'on vient vérifier. La carte affiche désormais, sous les cumuls, une ligne par magasin avec ses ventes, ses entrées et son stock de fin de mois. Uniquement en mode tous magasins : sur un magasin unique, les tuiles sont déjà ce détail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
6e59458403 |
fix(grille): grille vide au changement de magasin, et stock TOTAL incomplet
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
|
||
|
|
720cf3c7a1 |
feat(grille): filtre nomenclature en multi-sélection
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 |
||
|
|
0bd7ec9798 |
feat(admin): synchronisation nocturne des fournisseurs, paramétrable
Page /admin/synchronisation : choisir quels fournisseurs sont cadencés, pour quelle source, et suivre l'avancement. ORDRE DE PASSAGE. La file est « le plus anciennement synchronisé d'abord », en ignorant ceux déjà traités depuis l'ouverture de la fenêtre. Il en découle, sans curseur ni compteur de tour : la nuit 2 reprend là où la nuit 1 s'est arrêtée, le tour recommence quand tout le monde est passé, et un fournisseur en échec garde une date ancienne donc repasse en tête. DEUX FILES. SQL (~43 s sur le plus gros) et Qlik (jusqu'à 8 min, sur un serveur qui abandonne quand on le sollicite trop) ont leur propre activation et leur propre rythme. Une extraction Qlik est intercalée tous les N fournisseurs SQL, sinon elle ne démarrerait jamais avant que la file SQL soit vide. Un plafond par nuit et un âge minimal évitent de refaire chaque nuit des agrégats mensuels. Détails qui comptent : - La fenêtre signifie « ne plus DÉMARRER après l'heure de fin » : une extraction en cours n'est jamais coupée. Le passage par minuit est géré. - L'horodatage est écrit même en cas d'échec, sans quoi le fournisseur fautif bloquerait la file derrière lui. - Garde anti-boucle : l'avancement repose sur cette écriture, volontairement non bloquante ; si elle échoue, le round s'arrête au lieu de reprendre le même fournisseur indéfiniment. - Un fournisseur sans article est désactivé automatiquement, motif affiché, réactivable — plutôt que réessayé chaque nuit. - Désactivé par défaut : un déploiement ne doit pas se mettre à solliciter Qlik la nuit suivante sans décision explicite. Déclenchement par minuteur interne (instrumentation.ts), battement d'une minute. Aucune configuration côté hôte, se réarme au démarrage ; en contrepartie rien ne tourne si l'application est arrêtée. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
be8d576af5 |
chore: supprime les derniers restes de l'IA dans les Paramètres
La section « IA Copilot — OpenRouter » annonçait « pour les analyses de gammes », mais aucun code ne consommait cette clé : elle était décorative. Supprimée, ainsi que les routes devenues orphelines /api/admin/ai-config, /api/google-ai/models et /api/openrouter/models. Nettoyage de l'état correspondant dans la page : clé, liste de modèles, fournisseur d'IA et modèle Google ne faisaient plus qu'un aller-retour entre le chargement et l'enregistrement, sans aucune interface pour les modifier. handleSave n'enregistre donc plus que l'URL de la base. La règle set-state-in-effect s'est mise à signaler le `setIsMounted` du montage, inchangé mais devenu analysable une fois le code IA retiré : dérogation locale, comme dans api-connection-info.tsx, puisque poser l'indicateur au montage est précisément ce qui évite l'écart d'hydratation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
fff4cd9641 |
chore: retire la section Score des Paramètres et supprime la page Chat IA
- Section « Score Produit » retirée : elle ne faisait que décrire une formule, sans rien paramétrer. - Page /admin/ai-chat et sa route /api/admin/ai-chat supprimées, ainsi que l'entrée de menu et la section « Admin AI Chat — Fournisseur » des Paramètres, qui n'existait que pour la configurer. - Nettoyage de l'état devenu mort dans la page Paramètres : la liste des modèles Google et son chargement n'avaient plus d'interface. Effet de bord notable : tsc passe de 23 à 4 erreurs préexistantes — la page de chat en portait 19 à elle seule (flags de regex exigeant une cible es2018). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
310165151d |
fix(qlik): reprise globale quand le moteur abandonne l'extraction (code 15)
Deux exécutions du même fournisseur D005, même code, même jour : 04:17 → OpenDoc en 78 s, succès en 479 s 06:13 → OpenDoc en 233 s, « code 15 / Request aborted » après 333 s Le moteur Qlik est trois fois plus lent à 6 h qu'à 4 h et finit par abandonner la requête. Les reprises par requête de rpcWithRetry ne servent à rien ici : c'est toute l'extraction qui tombe, et la sync repartait à zéro sans réessayer. L'extraction est désormais relancée entièrement sur erreur transitoire (code 15 ou « Request aborted »), une fois par défaut, après une pause de 60 s — pas immédiatement : relancer aussitôt ne ferait qu'ajouter de la charge au serveur qui vient de renoncer. Réglable via QLIK_EXTRACTION_RETRIES et QLIK_EXTRACTION_RETRY_PAUSE_MS. Le reste du comportement est préservé : le refus de publier une fenêtre 12 mois incomplète, et la reprise sur point de contrôle hors fenêtre datée, s'appliquent comme avant une fois les reprises épuisées. Le checkpoint est vidé entre deux tentatives pour ne pas mélanger deux extractions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
0a57a9a707 |
fix(api): nomenclatures dérivées du préfixe de code3, pas de code1/code2
Les codes de nomenclature FF font 6 chiffres et sont hiérarchiques par préfixe (31xxxx à 40xxxx) : « 32 » univers, « 3202 » famille, « 320211 » sous-famille. L'endpoint groupait sur les colonnes code1/code2, qui ne sont renseignées que si pgGetNomenclatureByFournisseur a su remonter la hiérarchie — ce qui dépend d'une détection heuristique de la colonne parent de la table `nomenclature`. Quand elle échoue, seul code3 est rempli et l'endpoint aurait renvoyé une liste vide aux niveaux 1 et 2, sans rien signaler. Les niveaux sont désormais dérivés de left(code3, 2|4|6), toujours disponible. En conséquence, /grid gagne un filtre `nomenclature` par préfixe, qui fonctionne aux trois niveaux et ne dépend pas non plus de code1/code2 : c'est lui que meta.filtreGrid désigne. Les filtres code1..code3 exacts restent inchangés. Les libellés viennent toujours du payload et peuvent être null aux niveaux 1 et 2 pour la même raison ; codes et compteurs, eux, sont exacts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
60aa2ce120 |
feat(api): inventaire des nomenclatures, pour découper un gros fournisseur
Les filtres code1/code2/code3 existaient déjà sur /grid, mais un appelant ne pouvait pas savoir quelles valeurs existent chez un fournisseur : il aurait dû tout télécharger pour les découvrir, ce qui annulait l'intérêt du découpage. GET /api/v1/nomenclatures?fournisseur=…&niveau=1|2|3&parent=… renvoie les postes avec leur nombre d'articles et leur CA, agrégés en SQL — quelques dizaines de lignes, même pour un fournisseur de 130 000 articles. `parent` permet de descendre l'arborescence sans tout charger, et meta.filtreGrid indique le paramètre de /grid correspondant au niveau demandé. Le parcours devient : lister les postes, puis /grid?fournisseur=…&code1=… poste par poste. Bien plus praticable pour une IA que 132 000 lignes d'un bloc, et plus pertinent métier que le découpage par gamme. Les libellés sont lus dans le payload jsonb (ils n'ont pas de colonne dédiée). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
8fe294924b |
fix(api): tient les très gros fournisseurs (132k articles)
Le journal du fournisseur D005 montre 132 388 lignes persistées pour un seul
fournisseur. À cette échelle, /api/v1/grid sans limit ne tenait pas.
Deux plantages francs, indépendants de la taille de la réponse :
- getNetworkMetricsByCodeCentrale : inArray avec 132 000 valeurs, soit autant
de paramètres liés, alors que le protocole PostgreSQL en accepte 65535.
- pgGetGammesByCodeins : même problème sur son IN (…).
Les deux lectures sont désormais découpées par lots de 2000. Le bug existait
pour tout appel dépassant ~65 000 codes, indépendamment de l'API.
Volume : charger 132k lignes complètes puis les sérialiser d'un bloc représente
plusieurs centaines de Mo en mémoire. Sans `limit`, la réponse est maintenant
diffusée en flux, par lots internes de 1000, à mémoire constante. La forme
{ data, pagination, meta } est inchangée — un client existant ne voit que des
octets arrivant progressivement.
Une erreur survenant après les premiers octets ne peut plus changer le code
HTTP : le document est alors fermé avec meta.erreur et meta.complet=false, pour
qu'une réponse tronquée ne passe pas pour complète.
`fields` reste le levier décisif à cette échelle : une ligne complète porte les
séries mensuelles et les ventilations par magasin.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
|
||
|
|
33b30dc8bd |
fix(api): supprime le plafond de 500 lignes, qui tronquait en silence
Le passage à limit=5000 du commit précédent était sans effet : queryGridRows re-plafonnait à 500 en dur (Math.min(500, …)). Pire, la pagination était calculée sur le limit *demandé*, donc hasMore et meta.complet annonçaient une réponse complète alors qu'elle était tronquée — exactement la troncature silencieuse que ces champs devaient éliminer. - Plus aucun plafond. Sur /grid, omettre `limit` renvoie toutes les lignes du fournisseur : plus de pagination à dérouler, ce que les agents font mal. - La pagination reflète le mode sans limite (une page couvrant le total), donc hasMore et meta.complet disent la vérité. - Le défaut reste 100 sur les endpoints de recherche, non bornés par un fournisseur — mais sans maximum imposé. - meta.avertissement explique désormais que la troncature vient du `limit` fourni, et qu'il suffit de l'omettre. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
73accd6d60 |
feat(api): un seul appel suffit pour tout un fournisseur
Le plafond de pagination était de 500 lignes. Un lot fournisseur dépasse souvent ce seuil, si bien qu'une app ou un agent ne récupérait qu'une partie des données sans forcément s'en rendre compte — et les agents paginent mal, concluant volontiers qu'ils ont tout lu. - /grid accepte désormais limit jusqu'à 5000. La borne reste à 500 sur la recherche transversale, qui n'est pas bornée par un fournisseur. - pagination.hasMore, et meta.complet sur /grid : l'appelant sait s'il a tout, sans avoir à comparer page et totalPages. - Réponse partielle : meta.avertissement indique le nombre de lignes obtenues sur le total et comment obtenir le reste. Pas de troncature silencieuse. - Doc des Paramètres et openapi.json mis à jour, avec un exemple curl qui récupère un fournisseur entier. `fields` reste vivement conseillé : une ligne complète porte les séries mensuelles et les ventilations par magasin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
3fa92e891b |
merge: intègre main — main prioritaire, abandon de la page en doublon
main a beaucoup avancé (53 commits) pendant que cette branche construisait l'API. La fusion a révélé que /produits, sur main, fait la même chose que la page Recherche réseau d'ici, en plus abouti : fiche produit, opportunités, URL partageables, et sa propre chaîne Qlik (lib/qlik-search, pgSearchProduits). Résolution, main prioritaire : - sidebar, heatmap-grid, qlik-playwright : version de main telle quelle. Son travail sur la tendance et Qlik est postérieur et plus complet. - get-product-rows : import NB_MAGASINS_RESEAU de main, plus l'appel upsertGridRows d'ici — sans lui /api/v1 n'aurait aucune donnée à servir. - settings : les deux côtés (ServerLogs de main + les sections API d'ici). Suppression du doublon : page recherche-reseau, /api/qlik/search, ses types et composants. searchNetworkProductsPlaywright disparaît avec la version main de qlik-playwright ; rien d'autre ne l'utilisait. api-enrich se branche désormais sur le module de tendance de main (features/grid/lib/network-trend) plutôt que sur le mien, supprimé : sa mesure est meilleure — variation sur la quantité par magasin vendeur, ce qui distingue « vend mieux » de « est distribué plus largement » — et l'API renvoie ainsi exactement ce qu'affiche la Grille. Le champ trend gagne surQteParMagasin et nouveau. Apport conservé de la branche : l'API /api/v1, absente de main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
94e82aa404 |
fix(ff): le panneau API FF Nancy affichait une coquille vide + URL configurable
Deux problèmes distincts, dont un mal diagnostiqué au départ.
1. FORMAT DE RÉPONSE — cause réelle du « test ne donne rien ». Le serveur
fonctionne (vérifié : HTTP 200, synchro du jour, 33 tables), mais il renvoie
{ sync: [{ table_name, last_sync, rows_synced, status, error_msg }] } alors
que l'application lisait { lastSync, tables: [{ nom, derniereSync, nbLignes }] }.
Les champs n'existaient pas : le panneau s'affichait vide sans erreur, la
requête HTTP ayant réussi. normalizeSyncStatus() traduit désormais les deux
formes, et le tableau expose l'état par table (une table en erreur est
justement l'information qu'on vient chercher).
2. URL NON CONFIGURABLE. FF_API_BASE était figée au chargement du module depuis
process.env, sans champ dans l'interface : impossible de corriger l'adresse
sans redéployer. Elle est maintenant résolue à l'appel — réglage enregistré,
puis variable d'environnement, puis défaut — avec un champ, un bouton
Enregistrer et un test dans Paramètres.
Le test distingue désormais DNS, connexion refusée, délai dépassé et code HTTP,
et affiche l'adresse réellement appelée : un « HTTP 503 » nu ne permettait pas
de séparer une mauvaise URL d'une panne. /api/ff-status renvoie le même détail.
Ajout au passage du paramètre `compute` dans la doc API des Paramètres.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
|
||
|
|
18edda7167 |
feat(api): expose la tendance réseau calculée dans /api/v1
La colonne « Tendance » de la Grille était la seule information visible que
l'API ne fournissait pas : elle est calculée dans le navigateur par
computeNetworkTrend(), qui vivait dans un composant "use client" donc
inaccessible côté serveur. L'API livrait la série mensuelle brute et laissait
chaque appelant refaire la régression — au risque de diverger de l'affichage.
- Le calcul part dans src/lib/network-trend.ts (module pur, sans React) ;
le composant le réexporte, aucun import existant ne change.
- enrichRows ajoute `trend` : { direction, pct, label, monthsUsed }, calculé
sur la même série que la colonne de l'application.
- openapi.json : schéma Trend documenté, et il est dit explicitement que la
liste des propriétés de Product n'est pas exhaustive — la réponse porte la
ligne de grille entière.
Vérifié au passage que rien n'est perdu en chemin : upsertGridRows stocke le
ProductRow complet dans la colonne jsonb `payload` (les colonnes scalaires ne
servent qu'aux index), ne filtre que les lignes sans codein, et pickFields
renvoie tout tant que `fields` n'est pas précisé.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
|
||
|
|
3a825866d2 |
feat(api): /api/v1/grid calcule le fournisseur à la demande
L'API lisait uniquement la table grid_rows, écrite en effet de bord quand un
humain ouvre la page Grille. Un fournisseur jamais ouvert renvoyait donc
202 not_ready : une app ou un agent externe ne pouvait consulter que ce qui
avait déjà été parcouru dans l'interface, ce qui vide l'API de son intérêt.
/api/v1/grid déclenche désormais getProductRows() quand l'instantané manque.
Le calcul est déjà protégé en amont (cache 10 min + verrou anti-concurrence)
et persiste l'instantané, donc seul le premier appel paie le coût ; les
suivants repassent par le chemin SQL rapide.
- compute=1 (défaut) : calcul à la demande, meta.computedOnDemand signale
quand il a eu lieu. compute=0 : comportement strict d'avant.
- Fournisseur inconnu ou sans article : 404 explicite, distinct d'une panne
(500) et de « pas encore calculé » (202).
- /api/v1/products/{codein} fait de même lorsque `fournisseur` est fourni,
le calcul se faisant par lot fournisseur.
- openapi.json mis à jour : c'est ce document que lit un agent externe, il
annonçait « aucun endpoint ne déclenche de recalcul ».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
|
||
|
|
54d51e8eab |
fix(reseau): cast texte sur artcentrale pour la jointure code centrale
Le type réel de articles.artcentrale varie selon l'installation FF Nancy (varchar ou numérique). La comparaison avec des codes centraux (chaînes) passe désormais par TRIM(...::text), comme ailleurs dans ce fichier. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
c565672259 |
feat(reseau): page Recherche réseau — chercher dans le catalogue Qlik
La Grille part de NOTRE catalogue : on extrait nos codes centraux puis on demande à Qlik les métriques de ces codes. Un produit vendu par le réseau mais absent de FF Nancy était donc invisible par construction. Cette page inverse le sens : elle interroge Qlik d'abord, par libellé ou par code centrale, et affiche aussi les produits que nous ne référençons pas. - searchNetworkProductsPlaywright() : sélection par SelectValues sur « Article Code » (mode code) ou par SearchListObjectFor + AcceptListObjectSearch sur la dimension « Article » (mode plein texte, avec repli sur des noms de champs si la master dimension échoue). Cube [Article Code, Article, Mois] × 6 mesures, passe N-1 incluse pour obtenir 12 mois glissants contigus. Garde-fou à 300 produits/recherche. - /api/qlik/search : job asynchrone (POST + polling GET) comme la sync, une seule recherche à la fois pour ménager le moteur Qlik. Met les résultats en cache dans qlik_network_metrics et enrichit avec le catalogue local (badge « Chez moi » / « Réseau uniquement », stock). - Page /recherche-reseau : tableau triable, filtres, fiche produit avec la courbe 12 mois, export Excel. - Composants de tendance extraits de heatmap-grid vers features/network pour être partagés (comportement inchangé). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
4b229acc9b |
feat(api): API prête pour une IA externe type ChatGPT
Adapte l'API CollectFlow aux exigences des Actions ChatGPT et garantit que les données soient réellement disponibles pour un consommateur externe. Schéma OpenAPI : - openapi.json devient PUBLIC (sans clé). Il ne décrit que la structure, sans aucune donnée, or ChatGPT importe le schéma par URL avant que la clé ne soit configurée : l'exiger rendait l'import impossible. - servers[0].url est désormais ABSOLUE (une URL relative est rejetée à l'import), construite depuis l'hôte appelant ou COLLECTFLOW_PUBLIC_URL. - operationId sur chaque opération (requis par les Actions), schémas de réponse typés, et descriptions rédigées pour le modèle : quelle gamme utiliser pour raisonner, pourquoi préférer caParMagasinReseau au CA brut, que faire d'un 202 not_ready. Disponibilité des données : - Nouveau préchauffage /api/admin/grid-warmup + bouton dans Paramètres. Sans lui, l'API ne sert que les fournisseurs déjà ouverts à la main dans la Grille — une IA externe n'aurait presque rien vu. Le job calcule tous les fournisseurs séquentiellement (paralléliser saturerait PostgreSQL), saute ceux à jour depuis moins de 24 h et suit son avancement. Documentation : - Paramètres → marche à suivre pas à pas pour brancher un GPT (import du schéma, auth par clé personnalisée X-API-Key), et mention de COLLECTFLOW_PUBLIC_URL quand le domaine public diffère. L'assistant interne et son API api.ffnancy.fr ne sont pas touchés. Vérifié sur PostgreSQL local : schéma servi sans clé en 200, URL absolue, 5 operationId, auth apiKey/X-API-Key, surcharge COLLECTFLOW_PUBLIC_URL effective, 5 endpoints en 200 avec la clé, 401 JSON sans clé, et recherche transversale renvoyant bien plusieurs fournisseurs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
a45e09777b |
feat(api): métriques Qlik + gamme serveur à jour, et doc de connexion dans Paramètres
L'instantané grid_rows fige la ligne au moment du calcul, mais deux données évoluent indépendamment et doivent refléter l'état courant : - Métriques réseau Qlik : qlik_network_metrics est alimentée par les syncs et les recherches réseau, hors du calcul de grille. Relues à chaque appel et exposées dans `network` (null quand le produit n'en a pas). Les colonnes caReseau/qteReseau/... de la ligne sont réalignées dessus. - Gamme serveur : codeGamme peut être surchargée par un snapshot de session. L'API expose `codeGammeServeur`, la gamme NON modifiée telle qu'elle est en base PostgreSQL, relue à chaque appel. codeGammeInit est gardé aligné. Ajout de pgGetGammesByCodeins() : variante sans jointure artfou1, nécessaire car la recherche de l'API est transversale (pas de fournisseur connu). Ce n'est pas un recalcul : deux lectures indexées bornées à la page courante (500 lignes max). Mesuré à ~20 ms, soit le même coût que sans enrichissement. Paramètre enrich=0 pour servir l'instantané brut. Paramètres → nouvelle section « API CollectFlow — Connexion » : URL de base déduite de l'origine, en-têtes d'authentification, liste des endpoints et des paramètres, exemples curl copiables, lien vers openapi.json. Vérifié sur PostgreSQL local avec un instantané volontairement périmé : gamme snapshot A → serveur C, caReseau 111 → 990000, produit sans données réseau → network null, produit sans gamme → codeGammeServeur null, enrich=0 redonnant bien les valeurs figées, et toujours zéro recalcul. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
e79d7e51da |
feat(api): API CollectFlow /api/v1 — lecture de la grille et recherche
Les données de la grille (ventes 12 mois, stock, marges, gammes, métriques réseau Qlik) n'étaient accessibles par aucun moyen programmatique, et getProductRows() exige un fournisseur : chercher un produit sans le connaître était impossible. Contrainte de conception : l'API ne recalcule jamais rien. La grille était reconstruite en direct et gardée seulement 10 min en mémoire ; une API qui appellerait getProductRows() serait lente et imprévisible. On persiste donc le résultat d'un calcul qui a déjà lieu, et on le sert. - Table grid_rows : colonnes scalaires (filtre/tri/recherche en SQL) + payload jsonb du ProductRow complet. Remplie en effet de bord NON bloquant par getProductRows(), purge des articles disparus via computed_at. Survit aux redémarrages, contrairement au cache mémoire. - Endpoints /api/v1 : fournisseurs, grid, products/search (transversale, tous fournisseurs), products/:codein, network/:codeCentrale, openapi.json. Pagination, tri sur liste blanche, projection de champs, validation zod. 202 not_ready si un fournisseur n'a pas encore d'instantané. - Authentification double : clé d'API (X-API-Key ou Bearer, SHA-256 en base, révocable) ou session existante. Le middleware exempte /api/v1 — sans quoi un script recevait une redirection 302 vers /login au lieu d'un 401 JSON. - Gestion des clés dans /settings (server actions, clé affichée une seule fois). Vérifié contre une base PostgreSQL locale : 401 JSON sans clé, 401 sur clé révoquée, recherche renvoyant plusieurs fournisseurs, upsert + purge, et 24 appels /api/v1 sans déclencher un seul recalcul (l'ancienne route /api/grid/rows en déclenche un à chaque appel). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 |
||
|
|
150baeef9a |
Carte tendance : hauteur figee, bascule Graphique/Tableau, cellules copiables
1. LA CARTE CHANGEAIT DE TAILLE PENDANT LE SURVOL.
La lecture du mois vivait sous le graphique, en HTML : selon la longueur
des valeurs elle passait sur deux lignes et la carte sautait. Elle est
desormais dessinee DANS le SVG, dont la geometrie est figee par le
viewBox — infobulle ancree au reticule, qui bascule a gauche quand elle
deborderait a droite.
Deux causes residuelles trouvees a la mesure, pas a la lecture :
- le SVG etait dimensionne en pourcentage, donc sa hauteur dependait de
la largeur : il porte maintenant une hauteur en pixels ;
- un SVG en ligne traine l'espace sous la ligne de base (6 px mesures),
ce qui decalait la vue tableau : `display:block`.
Verifie en pilotant le composant reellement monte : 382,5 px au repos,
sur quatre positions de survol et dans les deux vues, en clair comme en
sombre.
2. LES VALEURS MOIS PAR MOIS SANS AGRANDIR LA CARTE.
Le deplaint aurait fait grandir la carte a l'ouverture — exactement ce
qu'on venait de corriger. Le tableau occupe donc la place du graphique
dans un panneau de MEME HAUTEUR et defile a l'interieur.
3. REFERENCE ET EAN COPIABLES, comme le code interne.
La copie etait ecrite en ligne dans la colonne « Code interne », avec un
`eslint-disable react-hooks/rules-of-hooks` : un useState dans le corps
d'un cell() de TanStack n'est pas rendu au meme endroit d'un rendu a
l'autre. Extraite en composant, la derogation disparait et les trois
colonnes partagent le meme comportement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
|
||
|
|
5ad222431d |
Carte tendance : refonte du graphique selon les regles data-viz
Petits multiples empiles sur un axe des mois commun, palette validee par script, etiquetage selectif, survol et vue tableau. FORME. Trois mesures d'ordres de grandeur differents (qte/magasin, volumes, magasins) ne peuvent pas partager un cadre : il faudrait deux ou trois echelles verticales dont l'alignement serait arbitraire, ce qui inventerait des correlations absentes des donnees. Trois facettes, une echelle chacune, le meme axe horizontal. La facette PRINCIPALE est la qte/magasin. COULEUR. Slots 1-3 de la palette categorielle de reference, en variables CSS, avec un jeu de pas CHOISI pour le fond sombre (pas un eclaircissement automatique). Palette passee au validateur dans les deux modes, toutes paires : bande de clarte, plancher de chroma, separation daltonienne (pire paire ΔE 9,2 clair / 9,4 sombre) et contraste. En mode clair l'aqua tombe a 2,82:1 — la regle de relief s'applique, d'ou etiquettes visibles ET vue tableau, toutes deux presentes. La teinte de tendance (vert/rouge/gris) ne vit plus que dans la tuile de verdict, avec fleche et libelle : un etat ne se signale jamais par la couleur seule, et la meme couleur ne veut pas dire deux choses. LISIBILITE. Etiquetage selectif — l'extreme et le dernier mois, pas les 36 valeurs. Le texte ne porte jamais la couleur d'une serie (illisible en teinte claire) : l'identite vient du repere pose a cote. Filets de base pleins et discrets, jamais pointilles. Reticule au survol reliant les trois facettes au meme mois, et vue tableau depliable : aucune valeur n'est accessible au seul survol. En-tete refait en tuiles : verdict, qte/magasin du dernier mois, magasins vendeurs. Chiffres proportionnels sur les valeurs mises en avant, tabulaires seulement dans le tableau ou les colonnes s'alignent. Rendu verifie en image dans les deux themes avant livraison : la version precedente faisait passer l'etiquette du pic par-dessus le titre de la facette — elle bascule desormais sous le point quand elle toucherait le bandeau. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu |
||
|
|
eaac945345 |
Tendance : la calculer sur la QTE PAR MAGASIN, pas sur le volume brut
C'est le bon indicateur, et le volume brut ne pouvait pas le remplacer : il confond la performance du produit et sa diffusion. Un produit monte en volume des qu'on le reference ailleurs, sans mieux se vendre nulle part ; un produit retire de 60 magasins s'effondre en volume tout en performant mieux la ou il reste. Seule la qte/magasin repond a « ce produit est-il tendance ». - computeNetworkTrend prend le nombre de magasins par mois et calcule l'evolution sur qte/magasins vendeurs. Sans cette serie, repli sur le volume brut et `surQteParMagasin` a false, pour que l'UI le dise. - La serie remonte jusqu'a tous les appelants : grille, fiche produit et recherche produits (nbMagByMonth ajoute au resultat de recherche). - La carte affiche desormais trois bandes empilees sur le meme axe : qte/magasin en PRINCIPALE (couleur de la tendance, aire pleine), quantites vendues et magasins vendeurs en contexte. La teinte « tendance » ne designe plus qu'une seule chose dans le graphique ; les bandes de contexte ont des couleurs fixes. - Le pourcentage de la carte precise « (qte/magasin) » : un chiffre de tendance sans son assiette se prete a tous les malentendus. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu |
||
|
|
e6e79dfe06 |
Carte tendance : desencombrer le graphique, il etait illisible
La densite etait le vrai probleme. La version precedente imprimait les 24 valeurs (12 quantites + 12 magasins) et titrait chaque bande DANS le SVG. Resultat sur ecran : les titres passaient par-dessus les courbes, les nombres des extremites etaient coupes par le bord du cadre, et les etiquettes des magasins se collaient au trait pointille. - Les titres sortent du dessin : legende HTML au-dessus, plus aucun recouvrement possible quelle que soit la forme des courbes. - La bande des magasins ne porte plus que sa DERNIERE valeur ; son niveau se lit a la forme et au maximum rappele dans la legende. - Les etiquettes des bords sont ancrees vers l'interieur (start / end au lieu de middle) : plus rien n'est tronque. - Chaque mois expose une infobulle native avec les valeurs exactes, ce qui rend inutile d'imprimer tous les nombres. - Cadre elargi (520 de large, marge haute pour les etiquettes du sommet) et modale passee en max-w-xl. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu |
||
|
|
bf6fef6a14 |
Carte tendance : deux bandes separees, indicateur lisible, courbe magasins reparee
Trois defauts signales, trois causes distinctes.
1. LA COURBE DES MAGASINS N'APPARAISSAIT PRESQUE JAMAIS.
L'extracteur ecrivait `{ qte: 0 }` sur les mois sans vente, sans
`nbMag`. La serie exigeant les 12 mois, elle disparaissait des qu'un
produit avait un mois creux — c'est-a-dire presque toujours. Un mois
PRESENT dans le detail est un mois extrait : s'il n'a pas de nbMag,
c'est que personne n'a vendu, donc zero. Corrige a la source ET a la
lecture, pour que les caches deja ecrits repartent sans re-sync.
2. LES DEUX COURBES ETAIENT ILLISIBLES SUPERPOSEES.
Deux echelles independantes placaient la courbe des magasins AU-DESSUS
de celle des quantites : cela se lit spontanement comme « il y a plus
de magasins que de ventes », un contresens. Les series sont desormais
tracees dans DEUX BANDES distinctes alignees sur le meme axe des mois.
On compare la forme de l'une avec la forme de l'autre, sans jamais
suggerer un rapport de grandeur.
3. LES POURCENTAGES N'AVAIENT PAS DE SENS.
La pente d'une regression rapportee a la moyenne donnait « -124 % »
(une baisse ne peut pas depasser -100 %), « +1 062 % » pour 2 unites
vendues une fois dans l'annee, et surtout « +508 % » sur des dizaines
de lignes d'affilee — la signature arithmetique d'une serie nulle
partout sauf le dernier mois, ou le rapport ne depend meme plus des
quantites. On compare maintenant deux moyennes de 4 mois : variation
qui se lit comme telle, plancher a -100 %, insensible au bruit d'un
mois isole. Sans base de comparaison, aucun pourcentage n'est
invente — le produit est marque « Nouveau », et le tri le remonte en
tete au lieu de l'enterrer.
Le graphique accepte aussi des quantites negatives (retours superieurs
aux ventes sur un mois) : l'echelle part du minimum reel, le point ne
sort plus du cadre.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
|
||
|
|
28fe78a646 |
Carte tendance : garantir que la courbe magasins se distingue des quantites
La courbe des quantites prend la couleur de la TENDANCE : vert, rouge, ou gris-bleu (#94a3b8) quand elle est stable. L'indigo des magasins se detache du vert et du rouge, mais il est trop proche du gris-bleu — or « stable » est le cas le plus frequent. storesColorFor() bascule sur l'ambre (#f59e0b) dans ce cas precis : gris froid contre orange chaud, impossible a confondre. Les deux series se distinguent desormais par TROIS canaux, pas seulement la couleur — qui ne suffit ni en impression noir et blanc, ni pour un daltonien : - teinte garantie differente, - trait pointille contre trait plein, - points evides contre points pleins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu |
||
|
|
abc704a363 |
Carte tendance : deuxieme courbe du nombre de magasins vendeurs
Une quantite qui monte parce que le produit est diffuse dans plus de magasins ne raconte pas la meme histoire qu'une quantite qui monte a diffusion constante. La courbe des quantites seule ne permettait pas de distinguer un succes produit d'un simple elargissement. - computeStoresSeries() : serie « magasins vendeurs » sur la MEME fenetre 12 mois que la tendance, avec la meme regle du tout ou rien — les 12 cles doivent etre presentes, sinon null. Un mois non extrait ne doit pas se lire comme « zero magasin », ce qui simulerait un arret de diffusion. - nbMagReseauByMonth remonte du cache (metricsByMonth, deja stocke par la sync) jusqu'a la ligne de grille. - NetworkLineChart accepte une serie `stores` optionnelle. DEUX ECHELLES INDEPENDANTES : les quantites se comptent en milliers, les magasins plafonnent a ~270. Sur une echelle commune la courbe des magasins serait ecrasee sur l'axe et ne montrerait rien. Chaque serie est normalisee sur son propre maximum, rappele dans la legende — c'est la FORME des deux courbes qu'on compare, pas leurs hauteurs. La courbe des magasins est pointillee et indigo, pour ne jamais entrer en collision avec le vert/rouge/gris de la tendance. Branchee sur la carte de la Grille et sur celle de la fiche produit. La sparkline de la colonne reste a une seule courbe : 46x18 px, deux traces y seraient illisibles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu |
||
|
|
b6225f1920 |
Qlik: la sync 12 mois passe — sonde fournisseur bornee, commentaires rectifies
Premier run reussi : 12 mois couverts, 40 411 produits ecrits dans le
cache, statut final=success.
Ce que la sonde paginee montre enfin :
Periode « (aucune) » → master exposee : 2024-01…2026-07 (31 mois)
Sans aucune Periode selectionnee, « Quantite N » couvre 31 mois, et une
seule passe (Annee=2026) sert les 12 mois de la fenetre. L'etat libre
etait donc le bon depuis le debut ; ce sont deux defauts de l'outil de
MESURE qui le cachaient — la sonde tronquee a 60 lignes, puis le Clear
appele sur le list object. Le commentaire de choisirPeriode affirmait
l'inverse (« sans Periode la master est morte ») : il est rectifie, avec
la mesure a l'appui.
Sonde fournisseur : elle lisait 1 207 205 codes en 240 pages avant de
conclure, trois fois de suite — 87 s pour un rejet joue d'avance. La
lecture s'arrete desormais des le plafond depasse.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
|
||
|
|
fee267004f |
Qlik: Clear/SelectValues sur le CHAMP Periode, pas sur un objet de session
Le journal complet (enfin lisible) donne la cause, et elle est unique :
choix de « Periode » impossible :
{"code":-32601,"parameter":"Clear","message":"Method not found"}
extraction par date brute interrompue :
{"code":-32601,"parameter":"Clear","message":"Method not found"}
Clear et SelectValues sont des methodes de CHAMP. Je les appelais sur le
list object « Periode ». Consequence : la cartographie des periodes ET le
chemin d'extraction par date brute n'ont jamais tourne — d'ou la
couverture master annoncee a 0/12 alors que la master vit (total mesure :
11 591 263).
- Handle du champ « Periode » obtenu comme ceux d'Annee et de Date ; le
list object ne sert plus qu'a ENUMERER les valeurs.
- appliquerPeriode selectionne par texte et ne propage plus d'erreur :
une periode qu'on n'arrive pas a poser doit degrader le resultat, pas
interrompre l'extraction.
- Chaque candidat de la cartographie est isole : une valeur en echec est
notee et ignoree, elle n'emporte plus toute la carte.
Sonde fournisseur : « J009 » laissait 1 207 205 articles visibles, soit
tout le catalogue — la selection ne restreignait rien, et la
« couverture » de 97 % ne faisait que constater que le catalogue contient
nos codes. Une selection qui laisse plus de 3x les codes demandes est
desormais rejetee immediatement (elle coutait 3 x 24 s de lecture).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
|
||
|
|
d89d2b3232 |
Parametres : telechargement des journaux serveur complets
Une extraction Qlik produit plusieurs milliers de lignes de diagnostic. Un terminal les tronque et n'en laisse que la fin, alors que l'information decisive — carte du modele Qlik, champs date retenus, choix des periodes — se trouve en TETE. Diagnostiquer sur la fin du log revient a travailler a l'aveugle. - src/lib/log-capture.ts : instrumente console.log/warn/error une fois, recopie chaque ligne horodatee dans les captures ouvertes, ecrit au fil de l'eau dans le repertoire temporaire (un job qui fait tomber le process laisse quand meme son journal). Bornes : 200 000 lignes, 8 000 caracteres par ligne, 20 fichiers conserves. - GET /api/logs : liste JSON ; GET /api/logs?id=<jobId> : le fichier complet en piece jointe. Admin uniquement. - La sync Qlik ouvre la capture AVANT son premier log et la ferme dans un finally, avec le statut final — les deux modes (fournisseur et produit). - Parametres : section « Journaux serveur », un bouton Telecharger par job, le plus recent en premier, badge « en cours » pour un job vivant. Aucun secret ne transite dans ces journaux : l'extracteur ne journalise que le NOM du cookie de session, jamais sa valeur ni le mot de passe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu |
||
|
|
50f883ec98 |
Qlik: extraction mois par mois sur le champ date brut, sans pont de periodes
Le log livre le fait qui manquait :
dimension « Mois » → 35602068 (100% du total sans filtre)
100 %, donc TOUTE la table de faits tient dans les 12 mois. Le test de
champ date « il doit faire baisser le total » etait donc structurellement
faux : n'importe quel champ correct donne 100 % quand la fenetre couvre
tout. Il rejetait les bons champs, ce qui condamnait le seul chemin sain
et renvoyait vers la dimension Mois — celle qui demultiplie les faits.
Nouveau chemin moisParDateBrute(), essaye AVANT l'agregation par
dimension :
- Type_Cal / Periode / Annee effaces : on veut les faits nus, pas un
contexte de calendrier. Plus aucun mois « inatteignable », puisqu'un
mois y est un filtre de dates.
- detection du champ date sur UN SEUL MOIS : valide si le total est non
nul ET une fraction du total (ni 0, ni 100 %).
- cube [Article Code] x 4 expressions, SANS dimension Mois : c'est elle
qui passe par le pont et rattache un fait a plusieurs contextes
(mesure : x5,3).
- 12 selections de dates, 12 cubes pagines, un point de controle par
mois.
Controle d'integrite : chaque mois doit etre servi, et la somme des 12
mois ne doit pas depasser le total sans filtre de date — une
demultiplication ferait exploser ce rapport. Sinon rien n'est ecrit.
Corrige aussi le meme test defectueux dans moisParExpressions, et permet
d'effacer Periode meme si son list object n'a pas pu etre cree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
|
||
|
|
b948aca2f8 |
Qlik: une Periode PAR ANNEE, et sonde de couverture paginee
Deux defauts, l'un cachant l'autre. 1. La sonde de couverture ne lisait que les 60 premieres lignes du cube [Mois] x Quantite N. La dimension Mois porte tout l'historique de l'app : les mois recents tombaient hors de la premiere page et la sonde annoncait 0/12 pour TOUTES les valeurs de Periode — alors que la master vivait (mesure dans le meme log : total 9 721 153). C'est ce faux zero qui faisait effacer Periode et condamnait l'extraction. Le cube est desormais pagine integralement, et la sonde journalise aussi ce que la master expose hors fenetre. 2. La bonne valeur de Periode DEPEND DE L'ANNEE. « Annee a date » convient a 2026 (janvier -> mois courant) mais pas a 2025, dont on veut les 12 mois. Le choix global ne pouvait donc satisfaire que l'une des deux passes — ce que montrait le log : 2026 servait 2026-01..06, la passe 2025 ne servait rien. choisirPeriode cartographie maintenant, pour chaque annee de la fenetre, la valeur qui sert le plus de mois attendus de cette annee-la (l'etat sans Periode etant un candidat comme un autre), et chaque passe annuelle pose SA valeur avant de selectionner l'annee. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu |
||
|
|
5fee5bc058 |
Qlik: ne plus effacer Periode — c'est elle qui donne son sens a Type_Cal
Le log de production tranche : sans selection de Periode, les master measures sont MUETTES. Les passes annuelles renvoyaient bien 270 056 lignes, mais toutes mesures nulles — d'ou « 0 point mensuel ajoute » puis le refus des 12 mois. choisirPeriode() jugeait chaque valeur de Periode SANS annee selectionnee. « Annee a date » y parait couvrir 6/12 mois, donc etait rejetee, donc Periode etait effacee — et tout mourait. Or avec Annee=2025 selectionnee, la meme valeur donne l'annee 2025 COMPLETE (l'app affiche « Du 02/01/2025 au 31/12/2025 »), et avec Annee=2026 elle s'arrete au mois courant. L'union des deux passes fait bien 12 mois. - La couverture d'une Periode est desormais mesuree comme l'UNION des passes annuelles, exactement comme monthDimPath les executera. - La meilleure valeur est CONSERVEE meme partielle ; seule une couverture nulle justifie d'effacer. - Annees traitees la plus recente d'abord (usage de l'app), arret des que la fenetre est complete. - Diagnostics : mois gagnes / encore manquants apres chaque passe, et liste des mois manquants dans les messages d'echec. - Une passe ne peut plus ecraser un mois servi par un zero. Deux defauts prouves par le meme log, cote agregation directe : - la master « Quantite N » morte vidait TOUT l'hypercube (4 expressions valides a 6 mois, cube complet a 0 ligne) : elle est desormais sondee et omise si morte ; - ventilee par Mois, Sum(quantite) totalise 179 574 616 contre 33 945 285 sans dimension (x5,3) : la dimension Mois passe par le pont de periodes et demultiplie les faits. Ce chemin est refuse plutot que d'ecrire des ventes reseau fausses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu |
||
|
|
fa1b64cfa6 |
Qlik: selection par code fournisseur au lieu de 41 569 codes articles
Le code fournisseur FF de la base SQL/API existe aussi dans Qlik : une seule valeur y designe le meme perimetre que les dizaines de milliers de codes articles. Comme SelectValues sur « Article Code » coute 8 a 100 s par appel et doit etre refait a chaque passe annuelle, la bascule supprime la partie la plus couteuse de la sync. - ETAPE 0 : selectionnerParFournisseur() avant choisirPeriode(). - Essaie QLIK_FIELD_CODE_FOURNISSEUR puis code_fournisseur, Fournisseur, fournisseur_code_centrale. - Verifie la couverture reelle (Article Code visibles vs codes demandes) et n'accepte la bascule qu'au-dela de QLIK_SUPPLIER_MIN_COVERAGE (0.99 par defaut) ; sinon annule la selection et revient aux codes. - Quand la bascule est retenue, plus aucun SelectValues sur Article Code (passes annuelles et agregation par expressions). - La sync fournisseur transmet job.fournisseur ; la sync produit (un seul code centrale) reste inchangee. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu |
||
|
|
e7a387b7de |
fix(qlik): effacer Période puis extraire année par année — la méthode de l'app
Deux remarques de l'utilisateur ont débloqué le sujet : « sur ce tableau j'ai 2024, 2025, 2026 et je sors mois par mois », et « tu mets 2026 et tu as la comparaison N-1 ». Type_Cal='N' ne désigne donc pas « l'année civile en cours » mais L'ANNÉE SÉLECTIONNÉE. Il suffit d'enchaîner les années couvertes par la fenêtre (2025 puis 2026) pour reconstituer 12 mois glissants — chaque passe livrant les 5 mesures, et non la seule quantité comme « Quantité COMP ». Ce mécanisme existait déjà (monthDimPath(yearN - 1)) mais restait sans effet : [qlik-pw] (mois) passe N-1 terminée : 299905 → 299905 points mensuels La sélection « Période », héritée de l'ouverture de l'app et jamais choisie par nous (observé « Période:1/4 » dès le premier diagnostic de la session), épinglait le contexte sur l'année en cours et annulait la sélection d'année. - choisirPeriode() teste désormais EN PREMIER l'état sans aucune sélection de Période — l'état dans lequel un utilisateur voit 2024/2025/2026 — et efface la sélection dès que la couverture est incomplète. - L'orchestration boucle sur les années de la fenêtre au lieu d'une passe principale plus une passe N-1 dérivée de COMP. - Chaque passe n'écrit que les mois DE LA FENÊTRE et les totaux s'additionnent sur ces mois : deux années ne peuvent pas se doubler puisqu'elles ne partagent aucun mois. « Quantité COMP » n'est plus utilisée. - L'agrégation directe des faits reste en secours, et le garde-fou d'intégrité reste actif : sans les 12 mois, le cache n'est pas écrit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu |
||
|
|
ccd60b119f |
fix(qlik): ne pas amputer les faits avec une Période partielle + valider chaque expression
Deux constats de la dernière sync.
1. AUCUNE valeur de « Période » ne porte 12 mois glissants — c'est mesuré :
« juin 2026 » 1/12, « Année à date » 6/12, « Mois à date » 0/12,
« Semaine 2026/30 » 0/12
Ce sont 4 contextes relatifs à aujourd'hui, pas des types de période. Les
master measures ne pourront donc jamais couvrir la fenêtre, et l'agrégation
directe des faits devient obligatoire.
D'où un bug du commit précédent : garder la meilleure Période (6/12) RESTREINT
les faits à 2026-01→06 et ampute l'agrégation directe, qui sait pourtant lire
tout l'historique. choisirPeriode() efface désormais la sélection quand la
couverture est incomplète, et ne la garde que si elle couvre les 12 mois.
2. Le cube à 5 mesures calculait 26 s puis rendait 0 ligne, alors que
Sum(quantite) seul donnait 35 424 324 : signature d'une mesure invalide qui
invalide tout l'hypercube, sans qu'aucun message ne dise laquelle.
validerExpression() teste maintenant chaque expression isolément sur un petit
cube [Mois] avant de construire le gros. Une expression invalide est remplacée
par la colonne neutre 0 — le cube reste exploitable et le log nomme la
coupable. Seule la quantité est bloquante.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
|
||
|
|
5f3760f03e |
merge: piloter Période + garde-fou d'intégrité + sélection par dimension Mois
Fusion de deux lignes de travail parallèles sur main, complémentaires et non concurrentes : |
||
|
|
726fb8418d |
fix(qlik): piloter le champ Période du modèle au lieu de lutter contre lui
Le dump des expressions donne enfin la cause de fond :
« Quantité N » = Sum({<Type_Cal={'N'}>} quantite)
« Quantité COMP » = Sum({<Type_Cal={'$(vPeriod_comp)'}…>} quantite)
« Magasin Ventes Nb N » = Count({<Type_Cal={'N'}>} Distinct ventes_code_site)
L'app n'est pas un modèle « faits + calendrier ». Un pont de périodes duplique
les lignes de faits : Type_Cal='N' marque celles de la période analysée, 'COMP'
celles de la période de comparaison, et le champ Période (4 valeurs, une
sélectionnée) décide de quelle période il s'agit. Tout en découle :
- sélectionner Date ne change rien (mesuré : 100 % du total avant comme après),
le périmètre venant du pont de périodes et non du champ date ;
- « Quantité N » ne couvre que la période courante, d'où août→décembre vides ;
- « Quantité COMP » ne couvre que les mois ayant un comparable, d'où
janvier→juillet de l'année précédente ;
- Sum(quantite) brut additionne les lignes N ET COMP : il double compte, il ne
pouvait pas servir de substitut.
choisirPeriode() essaie donc chaque valeur de Période, mesure sur un cube
[Mois] × Quantité N combien de mois de la fenêtre elle rend réellement
disponibles, et garde la meilleure. Aucun libellé n'est deviné : c'est la
couverture mesurée qui décide, et elle est tracée. Si aucune valeur ne couvre
les 12 mois, les mois manquants restent absents du cache.
Aussi :
- Les mois réellement couverts reçoivent un zéro EXPLICITE quand l'article n'a
pas vendu ; la tendance ne retient que les mois présents comme clés. Un article
vendu depuis mai voyait sa tendance calculée sur deux points (« +4100 % »).
- Les diagnostics du chemin d'extraction sont répétés en fin de sync
([qlik-pw][diag]) : ils étaient au début d'un log de plusieurs milliers de
lignes.
- QLIK_USE_EXPR passe à désactivé par défaut.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
|
||
|
|
cd2e77e909 | fix Qlik month dimension selection | ||
|
|
4ef0e9fdeb | fix Qlik rolling 12-month integrity | ||
|
|
db886cd8d9 |
fix(qlik): supprime la passe de rattrapage et vérifie que la sélection Date filtre
Le log de la dernière sync montre batches=3 et des lignes « (rattrapage) » : le chemin par expressions a bien tourné mais a été rejeté (quantité totale nulle), et c'est le rattrapage qui a de nouveau recopié le total de période dans les mois manquants — d'où les plateaux à 87 648 d'août à décembre et les « -162 % ». - Passe de rattrapage SUPPRIMÉE. Elle ré-extrayait un mois vide en ne sélectionnant que ses dates ; les master measures ignorant la sélection Date, le cube lui renvoyait le total de période. Un mois qu'on ne sait pas extraire doit rester absent, jamais rempli d'une valeur plausible. - Le chemin par expressions vérifie maintenant qu'un champ date filtre RÉELLEMENT les faits, ce que personne n'avait pu contrôler tant que seules des mesures insensibles à la sélection étaient utilisées. Un total de contrôle (Sum(quantite) sans dimension) est comparé avec et sans filtre ; un champ n'est retenu que si le total est non nul ET strictement inférieur au total global. Candidats : QLIK_DATE_FIELD, Date, Date calendrier, Date_Key (ce dernier sélectionné au format AAAAMMJJ). Si aucun ne filtre, l'extraction le dit clairement au lieu de produire une fenêtre fausse. - Diagnostic : échantillon BRUT du cube loggué avant tout filtrage (mois brut, mois normalisé, appartenance à la fenêtre, valeurs des cinq mesures), et compteur de lignes écartées comme hors fenêtre. Ces deux traces distinguent une expression invalide d'un format de dimension Mois inattendu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu |
||
|
|
f0e36308f2 |
feat(scripts): qlik-validate.mjs — valider les expressions et la clé de jointure
Le serveur Qlik est filtré par IP : injoignable depuis l'extérieur du réseau FF
(la passerelle d'egress tombe en connection timeout sur 443 comme sur 80, alors
qu'elle atteint le reste d'Internet). Impossible donc de valider à distance les
expressions par défaut ni la clé de jointure.
Ce script répond aux deux questions en une exécution, à lancer depuis le
conteneur de l'app :
- expressions réelles des master measures (CA N, Quantité N, COMP…) ;
- tableau mois par mois comparant Sum(quantite) / Sum(ca_ht) / Sum(ca_ttc) /
Count(DISTINCT [Magasin Code]) aux mesures « N », sur l'année en cours (où
elles doivent coïncider) et sur l'année précédente (où les « N » sont
censées être à 0) ;
- Article Code, article_no_centrale et article_codein côte à côte, pour
trancher la jointure avec articles.artcentrale.
Lecture seule : sélections en soft lock, objets de session détruits. Le mot de
passe vient de l'environnement, rien n'est écrit ni affiché.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
|
||
|
|
4a56584a27 |
fix(qlik): les master measures ignorent la sélection Date — agrégation directe des faits
Preuve arithmétique dans les logs : pour l'article 10000397784, qteReseau=164 et
caReseau=2220,75 € sont EXACTEMENT la somme de janvier à juillet 2026
(12+19+24+27+22+31+29 = 164). Et la passe de rattrapage, avec août 2025 seul
sélectionné, a renvoyé ces mêmes 164 / 2220,75 — puis les a recopiés dans les
cinq mois manquants.
« CA N » et « Quantité N » sont donc des mesures « cumul année en cours »,
insensibles au champ Date. Aucune sélection ne peut leur faire produire une
fenêtre 12 mois glissants. D'où, simultanément :
- des totaux réseau qui étaient un cumul année en cours, mois courant partiel
inclus, et non 12 mois glissants ;
- les mois de l'année précédente non couverts par « Quantité COMP » vides sur
tous les articles ;
- un rattrapage qui y recopiait le total de période.
Le chemin nominal agrège désormais directement les champs de faits, qui eux
respectent les sélections : Sum(quantite), Sum(ca_ht),
Count(DISTINCT [Magasin Code]), Sum(marge) — tous surchargeables par
QLIK_EXPR_QTE / _CA / _NBMAG / _MARGE. Un seul cube [Article Code, Mois] couvre
les 12 mois : ni passe N-1 ni rattrapage, c'est ce qu'ils compensaient. Les
totaux deviennent la somme des 12 mois de la fenêtre.
Garde-fous : « Quantité N » reste dans le cube pour calibrage et le log compare
les deux sur les mois de l'année en cours (seul périmètre où elle est juste) ;
quantité totale nulle ou erreur Engine → repli automatique sur l'ancien chemin ;
QLIK_USE_EXPR=0 le force.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
|
||
|
|
b2a426287f |
perf(qlik): sync 24 sélections → 1, et résultat partiel au lieu de tout perdre
Les timings de production sont sans appel : SelectValues sur « Article Code » coûte 8 à 100 s PAR APPEL, quasi indépendamment du nombre de valeurs (le moteur balaie un symbole de plus d'un million d'entrées). Avec 7 200 codes en lots de 300, cela faisait 24 appels, ~15 min de sync, et la session Qlik mourait avant la fin — « Socket closed » puis « Execution context was destroyed » — en perdant la totalité de l'extraction. - Le chemin mensuel fait désormais UNE seule sélection pour tous les codes et un seul cube paginé : la dimension Mois livre déjà tous les mois d'un coup, rien n'obligeait à découper. La lecture des pages coûte ~50 ms. Repli automatique sur les lots si l'Engine refuse (les lignes déjà lues sont retirées pour ne pas doubler les totaux). - rattraperMoisVides() adopte le même principe : une sélection de codes, puis seule la fenêtre de dates change d'un mois à l'autre. - Sonde de diagnostic de fin de sync retirée : elle refaisait une sélection complète des codes et des 365 jours pour rien. - Point de contrôle (cfCheckpoint) poussé après chaque passe. Si la session meurt, l'extraction repart de là : les codes déjà traités ont des données complètes, seul le reliquat manque. Le log signale « résultat PARTIEL ». - Le script in-page remonte aussi ses lignes en cas d'erreur interne, et « rows=[object Object] » dans les logs est corrigé. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu |
||
|
|
6b1caa8ee0 |
fix(qlik): mois vides sur toute la fenêtre glissante (août→déc pour tous les articles)
Cinq mois consécutifs à 0 sur TOUS les articles n'est pas un résultat métier. Les mesures « N » de l'app Qlik sont bornées à une année civile ; le chemin « dimension Mois » sélectionne les 365 jours d'un coup, donc la mesure ne se résout que sur une année et les mois de l'année précédente ressortent vides. « Quantité COMP » ne rattrape que les mois ayant un comparable dans l'année en cours — d'où le trou d'août à décembre 2025 observé en juillet 2026. - La passe principale n'écrit plus de 0 pour un mois hors année N, et COMP n'écrit rien quand elle est vide : un mois absent peut être rattrapé, un mois à 0 se lit comme « pas de vente » et masque le trou définitivement. - Nouvelle passe rattraperMoisVides() : tout mois resté vide pour tous les articles est ré-extrait en ne sélectionnant QUE ses dates. La mesure se résout alors sur la bonne année quelle que soit l'écriture de son set analysis. Elle ne touche que le mensuel, jamais les totaux de période. - Les expressions des master measures sont dumpées au log : sans elles, impossible de savoir comment la mesure est bornée dans le temps. Contournement sans redéploiement : QLIK_MONTH_DIM=0 (itération mois par mois, plus lente mais sans trou). Les données déjà en cache gardent leurs zéros — il faut relancer la sync pour les corriger. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu |
||
|
|
78b3d41896 |
docs(qlik): une seule recherche simultanée, pas deux
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu |