Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 50 additions & 9 deletions SUIVI.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,56 @@
> Tableau de bord. À mettre à jour au fil de l'eau — c'est le premier fichier à lire
> pour savoir où on en est.

**Les compteurs, mesurés le 29/07/2026 après la série de sept chantiers.** **2 572 tests
Go** verts sur 31 paquets — 0 échec, 5 écartés, contre 2 352 avant la série, dont 10
viennent du correctif du démarrage à froid intégré depuis `main` — et **764 tests
front** sur 34 fichiers, 0 échec, contre 686 sur 31 fichiers. `gofmt` propre, `go vet` vert,
`svelte-check` sur 338 fichiers sans une erreur ni un avertissement, et les deux gardes du
dépôt, `boundary` et `deps`, vertes. Le bundle de l'écran client reconstruit pèse **79 429
octets gzip, soit 70,5 % du budget de 112 640** — 33 211 octets de marge. La passe `-race`
**n'a pas pu être jouée sur ce poste** : elle exige cgo, le dépôt est en zéro cgo et il n'y
a pas de gcc ici. La CI Linux est seule à la couvrir, et c'est d'elle qu'il faut l'exiger.
**Les compteurs, mesurés le 30/07/2026 après le nommage des produits dans les
signalements.** **2 572 tests Go** verts sur 31 paquets — 0 échec, 5 écartés — et **766
tests front** sur 34 fichiers, 0 échec. `gofmt` propre, `go vet` vert, `svelte-check` sur
338 fichiers sans une erreur ni un avertissement, et les deux gardes du dépôt, `boundary`
et `deps`, vertes. Le bundle de l'écran client reconstruit pèse **79 425 octets gzip, soit
70,5 % du budget de 112 640** — 33 215 octets de marge. **La passe `-race` est verte elle
aussi**, 31 paquets, 0 échec.

**Il y a bien un gcc sur ce poste, et les relevés précédents disaient le contraire.** La
passe `-race` exige cgo, donc un compilateur C, et trois entrées de suivi de suite l'ont
déclarée injouable ici en la renvoyant à la CI Linux. C'était faux : WinLibs est installé
par winget, et son `gcc` — MinGW-W64 UCRT, 16.1.0 — n'est que dans le PATH **utilisateur**,
absent du PATH machine. Un shell qui n'hérite pas de l'environnement de session ne le voit
pas, et `go test -race` répond alors `cgo: C compiler "gcc" not found`, ce qui ressemble à
une absence. Le chemin, pour ne pas le rechercher une quatrième fois :

```
%LOCALAPPDATA%\Microsoft\WinGet\Packages\BrechtSanders.WinLibs.POSIX.UCRT_Microsoft.Winget.Source_8wekyb3d8bbwe\mingw64\bin
```

Les trois invariants de concurrence du Hub sont donc vérifiables **avant** de pousser, et
`make test` joue ses deux passes comme prévu — c'est exactement ce que l'en-tête du
`Makefile` demande de ne jamais perdre.

**Les listes de signalements nomment enfin les produits (30/07/2026).** §10.3 bis exigeait
le nom dans le OÙ depuis le début — *« `id` Odoo **et** nom du produit — de quoi ouvrir la
fiche sans chercher »* — et les trois listes de la page Catalogue n'affichaient que
l'identifiant. Corriger « 4412 » demande de chercher d'abord de quel produit il s'agit ;
corriger « AIL VIOLET SAF » commence tout de suite. Le nom est désormais porté par le
signalement lui-même, de la qualification jusqu'à l'écran.

| # | Ce qui a changé | État |
|---|---|---|
| 1 | `domain.Finding.ProductName`, rempli par les treize constructeurs de `internal/catalog` qui portent une ligne, et par `duplicateID` du parseur | ✅ |
| 2 | Migration `0002_findings_product_name.sql` : colonne `TEXT NOT NULL DEFAULT ''` sur `findings` | ✅ |
| 3 | `product_name` dans la route `GET /admin/api/imports` et dans `FindingDTO` | ✅ |
| 4 | Le nom dessiné avant l'identifiant dans les trois listes — anomalies, unités divergentes, non pesables | ✅ |
| 5 | Deux tests de store devenus indépendants du numéro de schéma (`MigrationCount()`), et `reopenWithMigrations` qui embarque **toutes** les migrations livrées | ✅ |

**Pourquoi un instantané et pas une jointure.** Aller chercher le nom dans `products` au
moment de l'affichage aurait évité la migration, et menti deux fois : les signalements d'un
import de mars auraient porté le nom d'aujourd'hui, et un lot **refusé** — dont aucun
produit n'entre jamais en base — n'aurait eu aucun nom du tout, alors que c'est précisément
le lot qu'on veut diagnostiquer. `weighings.product_name` existe pour la même raison depuis
le début.

**Ce qui n'a pas été touché, et pourquoi.** Le panneau « Produits retirés depuis l'import
précédent » ne publie encore qu'un **nombre** : les noms demanderaient une route de plus,
et le panneau dit lui-même où les lire. Ce n'est pas une liste de produits, c'est un
compteur — il sort du périmètre de cette correction.

**Le démarrage à froid de la v0.6 montrait une panne qui n'existait pas (29/07/2026).**
Poste redémarré après l'installation : session `openscale` ouverte seule, puis une page
Expand Down
12 changes: 10 additions & 2 deletions docs/02-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -2112,7 +2112,9 @@ flowchart TD
- **`IMAGE_MISSING`** comme anomalie de catalogue : voir §10.7. Une tuile sans photo est un **état d'affichage**, pas un défaut d'import — et sur les fichiers réels, `IMAGE_MISSING` se déclencherait sur **49 %** des lignes de `flv.csv` et **100 %** de celles de `flv_1.csv`. *(`IMAGE_INVALID`, en revanche, **revient** : depuis que le champ est rempli, un contenu base64 qui n'est ni JPEG, ni PNG, ni GIF, ni BMP est un fait signalable — §10.7.)*
- **`products.visible`** (colonne dérivée stockée) et **`products.anomalies`** (JSON doublonnant la table des signalements) : remplacés par `qualification` + `reason` (§12.3).

**Les signalements sont conservés par import** (`findings`, §12.3), avec ligne CSV, code, valeur fautive et phrase française — au lieu du `RapportIntegrite` purgé à chaque exécution. Deux signalements ne portent sur aucun produit en particulier : `UNEXPECTED_HEADER` (§10.2) et `UNKNOWN_CATEGORY` (§10.2 bis) ; ni l'un ni l'autre n'empêche quoi que ce soit.
**Les signalements sont conservés par import** (`findings`, §12.3), avec ligne CSV, identifiant Odoo, **nom du produit**, code, valeur fautive et phrase française — au lieu du `RapportIntegrite` purgé à chaque exécution. Deux signalements ne portent sur aucun produit en particulier : `UNEXPECTED_HEADER` (§10.2) et `UNKNOWN_CATEGORY` (§10.2 bis) ; ni l'un ni l'autre n'empêche quoi que ce soit.

Le nom est un **instantané d'affichage**, écrit par l'import qui l'a lu, et pour la raison qui vaut déjà pour `weighings.product_name` : le nom bouge dans Odoo, la ligne qui décrit un import de mars ne bouge pas. Aller le chercher dans `products` au moment de l'affichage montrerait, des signalements d'un import passé, un fait que cet import n'a pas dit — et n'en montrerait aucun pour un lot refusé, dont aucun produit n'est jamais entré en base. Il est vide dans les deux cas où le fichier n'en donne pas : un signalement qui ne porte sur aucun produit, et une ligne trop abîmée pour porter un nom, ce que `UNREADABLE_ROW` dit déjà.

#### 10.3 bis Le rapport d'import est un outil de correction d'Odoo, pas seulement un filtre

Expand Down Expand Up @@ -3006,6 +3008,12 @@ CREATE TABLE local_decisions (
CREATE TABLE findings (
import_id INTEGER NOT NULL REFERENCES imports(id) ON DELETE CASCADE,
csv_line INTEGER NOT NULL, product_id TEXT,
product_name TEXT NOT NULL DEFAULT '', -- instantané d'affichage, comme
-- weighings.product_name : le nom bouge dans
-- Odoo, la ligne qui décrit un import ne
-- bouge pas. Vide quand le fichier n'en donne
-- pas — signalement sans produit, ou ligne
-- trop abîmée pour porter un nom
code TEXT NOT NULL, -- INVALID_BARCODE, UNIT_MISMATCH, …
issue TEXT NOT NULL CHECK (issue IN ('anomaly','info')),
message TEXT NOT NULL, value TEXT NOT NULL DEFAULT ''
Expand Down Expand Up @@ -3822,7 +3830,7 @@ Contrastes **AAA** (≥ 7:1) sur tout texte ≥ 24 px, **AA** partout ailleurs
| Matériel | Balance : **protocole** (deux entrées, §9.3), **liste déroulante des ports détectés avec description USB**, **« Détecter automatiquement »** (ouvre chaque port 3 s, applique le parseur, annonce « COM8 : 12 trames valides, GRAM XFOC » — **c'est la détection qui répond à « y a-t-il une balance ? », pas l'exploitant**), case **« ce poste n'a pas de balance »** (`scale.present`, qui éteint le feu au lieu de le laisser rouge), **visualiseur des 20 dernières trames brutes en direct** (hexa + décodé, **toujours actif** : ce n'est plus un réglage), **cadence médiane**. Imprimante : driver, transport, **« Lister les files »**, **« Rechercher l'imprimante »**, statut, 3 auto-tests, trame de statut brute en hexa. |
| Étiquette | Menu de gabarit, **aperçu PNG rafraîchi à chaque frappe** (identique à l'impression, A2), décalage X/Y en dots avec flèches ±1 **bornées par la géométrie**, noircissement, vitesse, exemplaires, **bandeau chiffré de `Diagnose()`** avec la mention « troncature volontaire (ADR-003) », éditeur tabulaire du gabarit. |
| Règles | Grille de tarifs (code/libellé/abrégé/**remise en %**/ordre), **la ligne du tarif de référence en lecture seule pour son prix et modifiable pour ses mots**, les deux arrondis **avec l'explication de l'écart au centime**, les 14 garde-fous avec **leur seuil et leur message modifiables, leur sévérité en lecture seule** (§6.4), **aperçu en direct** (« à 8 g, ce produit serait refusé »), code-barres. **La liste des dérogations de poids minimum actives** — un produit nommé par ligne, avec son `min_weight_g`, sa date et son auteur —, en lecture ici et **modifiable depuis l'onglet Catalogue**, où se trouve le produit (§10.6). |
| Catalogue | **Où le poste va chercher le catalogue** — répertoire local ou serveur WebDAV, avec le répertoire surveillé, ou l'adresse et le compte (§10.1, ADR-038) ; le mot de passe s'y saisit **en écriture seule**, laissé vide il ne bouge pas, et choisir WebDAV avertit **au moment du choix** que le dépôt d'un CSV depuis l'écran n'y existe plus · Dernier import et son **inventaire** · **liste des anomalies et des unités divergentes, chacune avec son numéro de ligne du CSV, son motif en français et la valeur fautive** · **liste des non-pesables** (préemballés, sans code-barres), présentée comme un **inventaire neutre** et jamais comme une liste d'erreurs · produits **retirés** depuis l'import précédent (§10.9) · historique des 20 derniers imports · **glisser-déposer d'un CSV (A4)** · **interrupteur « Ne plus proposer ce produit »** · **« Autoriser ce produit à peser moins de 10 g »** (dérogation par produit, §10.6) · « Oublier la quarantaine » · état de la source. |
| Catalogue | **Où le poste va chercher le catalogue** — répertoire local ou serveur WebDAV, avec le répertoire surveillé, ou l'adresse et le compte (§10.1, ADR-038) ; le mot de passe s'y saisit **en écriture seule**, laissé vide il ne bouge pas, et choisir WebDAV avertit **au moment du choix** que le dépôt d'un CSV depuis l'écran n'y existe plus · Dernier import et son **inventaire** · **liste des anomalies et des unités divergentes, chacune avec le nom du produit, son numéro de ligne du CSV, son motif en français et la valeur fautive** · **liste des non-pesables** (préemballés, sans code-barres), nommés eux aussi, présentée comme un **inventaire neutre** et jamais comme une liste d'erreurs · produits **retirés** depuis l'import précédent (§10.9) · historique des 20 derniers imports · **glisser-déposer d'un CSV (A4)** · **interrupteur « Ne plus proposer ce produit »** · **« Autoriser ce produit à peser moins de 10 g »** (dérogation par produit, §10.6) · « Oublier la quarantaine » · état de la source. |
| Journal | 200 dernières pesées, filtres, export CSV, détail **avec la trame brute** et bouton « Rejouer cette trame ». Journal technique en dessous. |
| Poste | Numéro, nom, coop, empreinte, export/import avec **aperçu du diff champ par champ**, 5 versions restaurables, version du binaire, chemins, espace disque. *(Pas de bouton « redémarrer » : aucun bloc de configuration ne l'exige — §11.4.)* |

Expand Down
4 changes: 2 additions & 2 deletions docs/03-glossaire.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ portent.
| Un rayon de la grille | `Category.Code` / `Label` / `Rank` / `Color` / `Visible` |
| L'enregistrement d'une pesée au journal | `Weighing.ID` / `OccurredAt` / `Station` / `JobID` / `IdempotencyKey` / `ProductID` / `ProductName` / `Reference` / `Mode` / `GrossWeight` / `Tare` / `NetWeight` / `Quantity` / `BaseUnitPrice` / `Lines` / `Barcode` / `Source` / `Stability` / `RateMS` / `Frame` / `Result` / `Detail` / `DurationMS` · `WeighingLine.TierCode` / `UnitPrice` / `Amount` |
| L'enregistrement d'un import | `Import.ID` / `OccurredAt` / `Source` / `FileName` / `SHA256` / `ByteCount` / `RowsRead` / `UnreadableRows` / `Weighable` / `NotWeighable` / `Anomalies` / `UnitMismatches` / `ImagesDecoded` / `ImagesRejected` / `ProductsWithdrawn` / `Result` / `Code` / `Reason` / `DurationMS` |
| Ce qu'un import a à dire sur une ligne | `Finding.ImportID` / `CSVLine` / `ProductID` / `Code` / `Issue` / `Message` / `Value` |
| Ce qu'un import a à dire sur une ligne | `Finding.ImportID` / `CSVLine` / `ProductID` / `ProductName` / `Code` / `Issue` / `Message` / `Value` |
| Une photo, une décision humaine, un contenu banni | `Image.SHA256` / `ByteCount` / `Format` / `Width` / `Height` / `SeenAt` · `LocalDecision.ProductID` / `Offered` / `MinWeightG` / `Reason` / `DecidedAt` / `DecidedBy` · `QuarantineEntry.SHA256` / `FailureCount` / `FirstFailureAt` / `LastFailureAt` / `Code` / `Reason` |
| Les blocs de la configuration, un champ par bloc de §11.2 | `Config.Version` / `Readme` / `ModifiedAt` / `Station` / `Network` / `UI` / `Scale` / `Printer` / `Pricing` / `Barcode` / `Limits` / `Stability` / `Catalog` / `Journal` / `Admin` / `Maintenance` |
| Le contenu de chaque bloc | `StationConfig.Number` / `Name` / `Coop` · `NetworkConfig.Listen` / `AdminOnLAN` · `UIConfig.Language` / `Sound` / `IdleTimeoutSeconds` / `ReprintWindowSeconds` / `ShowGridPrices` · `ScaleConfig.Type` / `Present` / `ManualEntryAllowed` / `DegradeAfterSeconds` / `Options` · `PrinterConfig.Type` / `Template` / `Options` · `CatalogConfig.Type` / `Options` / `Images` / `Categories` / `FallbackCategory` · `ImagesConfig.Source` / `Path` · `JournalConfig.MaxRows` / `MaxDays` / `MaxTechnical` · `AdminConfig.PasswordHash` / `RecoveryCodeHash` / `SessionMinutes` / `AttemptsPerMinute` · `MaintenanceConfig.WeeklyIntegrityCheck` / `DiskAlertMB` · `BarcodeConfig.VerifyReferenceCheckDigit` |
Expand Down Expand Up @@ -519,7 +519,7 @@ portent.
| `qualification IN ('pesable','non_pesable','anomalie')` | `qualification IN ('weighable','not_weighable','anomaly')` |
| `idx_produits_grille` / `idx_produits_reference` | `idx_products_grid` / `idx_products_reference` |
| table `decisions_locales (produit_id, proposer, poids_min_g, motif, horodate, par)` | table `local_decisions (product_id, offered, min_weight_g, reason, decided_at, decided_by)` |
| table `signalements (import_id, ligne_csv, produit_id, code, issue, message, valeur)` | table `findings (import_id, csv_line, product_id, code, issue, message, value)` |
| table `signalements (import_id, ligne_csv, produit_id, nom_produit, code, issue, message, valeur)` | table `findings (import_id, csv_line, product_id, product_name, code, issue, message, value)` |
| `signalements.issue IN ('anomalie','information')` | `findings.issue IN ('anomaly','info')` |
| `idx_signalements_import` | `idx_findings_import` |
| table `pesees` | table `weighings` |
Expand Down
11 changes: 6 additions & 5 deletions internal/catalog/csvodoo/csvodoo.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,11 +396,12 @@ func (o Options) checkReadable(batch *ports.Batch) error {
// flv_1.csv.
func duplicateID(row catalog.Row, first int) domain.Finding {
return domain.Finding{
CSVLine: row.Line,
ProductID: row.ID,
Code: domain.FindingUnreadableRow,
Issue: domain.IssueAnomaly,
Value: row.ID,
CSVLine: row.Line,
ProductID: row.ID,
ProductName: row.Name,
Code: domain.FindingUnreadableRow,
Issue: domain.IssueAnomaly,
Value: row.ID,
Message: fmt.Sprintf("Corriger l'identifiant : « %s » est déjà porté par la ligne "+
"%d. L'identifiant Odoo est la clé du produit — deux lignes qui le partagent "+
"font disparaître la première sans un mot.", row.ID, first),
Expand Down
Loading