diff --git a/CLAUDE.md b/CLAUDE.md index 9a3e25e..237f82d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2602,3 +2602,194 @@ implémente donc `IAsyncDisposable` et libère aussi lorsqu'elle change d'URL d'image, donc `loading="lazy"` ne déclenche jamais le décodage. Ce n'est pas un défaut de la couverture — le blob a été décodé à la main par `createImageBitmap` pour le prouver. Ne pas partir en chasse là-dessus. + +## Lots I à P — retours d'usage du 2026-08-20, traités le 2026-08-21 + +Onze items, cinq familles. Ce qui suit ne redit pas ce que le code montre : seulement les +décisions, et ce qui a failli être fait de travers. + +### K2 — l'ISSN prend son tiret, et c'est la forme RANGÉE EN BASE + +C'est le seul point où le projet s'écarte de la règle « la valeur stockée reste nue », tenue +pour l'ISBN, et il y a une raison précise : `CodePeriodique.IssnDepuis` produit **déjà** un ISSN +à tiret depuis le code-barres, et la BnF interroge `bib.issn` avec le tiret. Un ISSN tapé +« 24666718 » ne se rapprochait donc de **rien** — ni de la revue déjà créée par un scan, ni +d'une notice. + +⚠️ La canonisation a lieu **avant la recherche** dans `ServiceRevues`, pas seulement à +l'écriture. Vérifié en exécution : `POST /api/revues` avec `24666718` puis avec `2466-6718` +retombent sur la **même fiche** (id 1), là où le second aurait créé une seconde revue. + +`FormatageIssn` est bien plus simple que `FormatageIsbn`, et il faut voir pourquoi : un ISSN se +coupe **toujours** au même endroit, alors que les tranches d'un ISBN dépendent du groupe puis de +l'éditeur. Aucune table embarquée, aucune coupure ne peut être fausse. Comme pour l'ISBN, ce qui +n'est pas un ISSN ressort **intact** — un code mal recopié doit se voir mal recopié. + +⚠️ `ServiceRenormalisation` rattrape les ISSN existants, et **pas** par le mécanisme générique : +la clé est *nullable*, or un jeu de clés confondrait tous les `NULL` en une seule valeur et la +deuxième revue sans ISSN bloquerait la première. D'où `CanoniserLesIssn`, avec la même règle de +collision que partout ailleurs — la fiche dont la forme canonique est déjà prise **garde la +sienne**, et le serveur démarre. + +Les champs de **saisie** gardent la valeur tapée : découper à la frappe se battrait avec le +curseur, et c'est la règle déjà actée pour l'ISBN. + +### O1 — la couverture d'un numéro est une URL, jamais des octets + +**Tranché avec l'utilisateur le 2026-08-21.** `NumeroRevue.CoverUrl` se colle à la main : +aucune source ne peut la fournir, l'ISSN désignant la **revue** et non la parution. **K1 (photo +de couverture depuis la caméra) n'est pas fait** : ce serait le premier stockage de fichiers du +projet, ce qu'« ebooks : fiches uniquement » écarte — espace disque YunoHost, sauvegardes plus +lourdes, et un cache hors-ligne dont la clé est une URL. + +⚠️ **Le piège, et il est silencieux** : le garde de `GET /api/couvertures` n'autorisait que +`Livres.CoverUrl` et `LivresSouhaites.CoverUrl`. Oublier `NumerosRevue.CoverUrl` n'aurait produit +**aucune erreur visible** — seulement une image qui s'affiche en ligne et jamais hors-ligne, +c'est-à-dire exactement le défaut corrigé le 2026-08-20 pour les hébergeurs sans CORS. Un test +le verrouille, et le relais a été vérifié en exécution sur une couverture de numéro (**200 +image/jpeg, 43 921 o**, deux redirections traversées). + +⚠️ Un `PUT /api/revues/numeros/{id}` a dû être ajouté, sans quoi couverture et une n'auraient +existé **qu'à la création, c'est-à-dire jamais** : on note un numéro le jour où on le range, on +en recopie le sommaire plus tard. + +### O2 — les articles à la une : table à part, séparateur point-virgule + +**Sur le NUMÉRO**, jamais sur la revue (déjà tranché) : les articles à la une changent à chaque +parution. + +**Table `ArticleUne` à part, et non `Theme`.** Un thème est un vocabulaire qu'on *réutilise* — +« dark fantasy » revient sur dix livres — alors qu'un titre d'article est unique à sa parution. +Rangés dans `Themes`, ils rempliraient de bruit un vocabulaire qui sert le catalogue. + +⚠️ Conséquence directe qu'on manque en recopiant le modèle voisin : **pas de n-n**. Une simple +clé étrangère vers le numéro suffit, puisque rien ne se partage. Unicité `(NumeroRevueId, +TitreNormalise)` — deux parutions peuvent parfaitement titrer pareil, un test le verrouille. + +**Le séparateur est le point-virgule**, et la question posée dans IDEES.md a été tranchée avec +l'utilisateur : **les thèmes de livres s'alignent dessus**, la virgule restant acceptée en repli. + +| Champ | Sépare sur | Pourquoi | +|---|---|---| +| Auteurs | `;` | déjà le cas depuis toujours | +| Thèmes | `;` **et** `,` | un thème ne contient jamais de virgule ; la virgule était l'habitude | +| Articles à la une | `;` **seul** | « Ukraine, deux ans après » serait coupé en deux | + +Trois champs voisins du même formulaire ne doivent pas se saisir de trois façons. `ListeSaisie` +porte le découpage une seule fois, avec ce paramètre pour unique différence. + +⚠️ Le service **remplace** les articles, il ne les fusionne pas : la ligne de saisie *est* la +liste. Un titre effacé du champ disparaît, comme pour les thèmes. + +### P — ranger une envie dans une série : le geste est du côté de la SÉRIE + +Choisi avec l'utilisateur, pour la raison donnée dans IDEES.md : c'est là qu'on voit l'ordre de +lecture, donc là qu'on sait quelle position donner. + +⚠️ **Rien ne relie l'envie à la place en base, et c'est tout le sujet.** Les séries sont +**communes** au foyer, la liste d'envies est **personnelle**, et son sens même est de préparer +un cadeau sans que l'autre le voie venir. Une `ElementSerie.LivreSouhaiteId` afficherait « tome 3 +souhaité par untel » à tout le monde. Le geste crée donc une place **ordinaire** — `LivreId` à +`NULL`, titre repris de l'envie — et **n'épargne qu'une resaisie**, ce qui est exactement la +demande. + +Ce qui doit rester invisible est le **lien**, pas l'existence du tome : on l'aurait saisi à la +main de toute façon. + +**L'envie survit au rattachement**, dans le prolongement exact de « une envie déjà au catalogue +est signalée, jamais supprimée » — et à plus forte raison ici, puisque rien n'a été acheté. +L'écran le dit, sans quoi on croirait avoir déplacé quelque chose. + +⚠️ Les envies dont le titre est **déjà** un tome de la série ne sont pas proposées : la place +n'étant identifiée que par son titre, les offrir mènerait droit au doublon. + +### J1 et J2 — le scan là où il manquait + +`ScannerCodeBarres` était déjà un composant autonome : les deux items sont donc du raccordement, +pas du décodage. + +- **J1, depuis une place vide d'une série** : le scanner s'ouvre **dans la place visée**, sous le + champ ISBN existant. ⚠️ C'est ce qui répond à l'exigence « le retour doit ramener sur la série + **et** sur la place » — il n'y a pas de retour, on n'a jamais quitté la place. Passer par + `/ajout` obligeait à revenir rattacher à la main, donc à risquer le mauvais tome. Les revues + (`977`) restent hors du flux, `ElementSerie.LivreId` ne pointant que vers `Livre`. +- **J2, depuis « ajouter une envie »** : même composant, sans création de `Livre` au bout. ⚠️ + L'add-on EAN-2 est **ignoré** ici, et ce n'est pas un oubli : il ne concerne que les revues, + qui ne se souhaitent pas. + +### I1 et I2 — la bibliographie + +- **I1, « Tout cocher »** vit **à côté du filtre**, et non dans la barre de sélection — celle-ci + n'apparaît qu'une fois une case cochée, c'est-à-dire trop tard pour rendre service. ⚠️ « Tout » + signifie **ce qui est actuellement visible** : après filtre, et selon que les masquées sont + affichées. Cocher les 200 notices remontées alors que l'écran n'en montre que trente serait + précisément ce que les compteurs par bouton cherchent à éviter. +- **I2, la couverture d'une œuvre non possédée** n'est rendue **qu'au dépliage**. Une + bibliographie compte des dizaines de lignes : les charger d'avance ferait payer des images que + personne ne regarde. La règle « sans ISBN, pas de couverture, et on n'en invente pas » est + intacte — la formule OpenLibrary s'applique à l'ISBN déjà repris du `dc:identifier`. + +### L1 — l'attente se voit + +`Patience.razor` : une rondelle et une phrase, `role="status"`. Posé sur le lookup ISBN, les +recherches BnF, la bibliographie et les nouveautés. + +⚠️ **Un indicateur ne remplace pas un message d'échec** : une source muette garde ses +`EtatSourceBibliographie` et ses motifs. Ici on dit « ça travaille », jamais « ça a marché ». + +Le besoin n'est pas décoratif, et le lot H l'avait montré : un écran qui ne dit rien pousse à +recliquer, donc à **relancer** l'appel. L'animation est neutralisée sous +`prefers-reduced-motion`, l'indicateur restant visible — c'est lui qui porte l'information, pas +sa rotation. + +### M1 — champ et bouton sur une ligne + +⚠️ `min-width: 0` sur le champ est ce qui fait tenir la ligne à 320 px : sans lui, un élément de +formulaire refuse de rétrécir sous sa largeur intrinsèque et pousse le bouton hors de l'écran. Le +bouton ne rétrécit pas et son libellé ne se coupe pas — un bouton tronqué ne se lit plus. Le +libellé est passé à « Créer », plus court, la phrase d'aide au-dessus disant déjà de quoi il +s'agit. + +### ⚠️ `dotnet ef` n'a plus besoin de démarrer l'application + +`FabriqueDbContextConception` (un `IDesignTimeDbContextFactory`) a été ajouté. Sans elle, +`dotnet ef` exécute `Program.cs` pour retrouver les services — or `Program.cs` appelle +`Database.Migrate()` au démarrage. **Écrire** une migration supposait donc d'ouvrir la base de +développement : au mieux inutile, au pire bloquant. Constaté ici, sur un système de fichiers +réseau où le verrou SQLite ne se prend jamais — l'outil attendait cinq minutes puis renonçait, +en laissant un processus qui verrouillait les binaires. + +⚠️ Sa chaîne de connexion ne sert **qu'à la génération du code** : produire une migration +n'ouvre aucune base. Elle n'a pas à correspondre à quoi que ce soit, et surtout pas à la +production, configurée par `ConnectionStrings__MaBibli` dans l'unité systemd. + +### Ce qui n'est délibérément pas fait, et pourquoi + +| Item | Raison | +|---|---| +| **A5** — alléger les couvertures (WebP, redimensionnement) | à **mesurer** avant de s'y engager, même règle que l'AOT WASM. Aucune mesure sur un fonds réel | +| **K1** — photographier la couverture | premier stockage de fichiers du projet ; écarté avec l'utilisateur au profit de l'URL collée | +| **N2** — stocker l'URL finale après redirections | plus urgent depuis que le cache est consulté en ligne ; et rien ne garantit qu'une URL d'`archive.org` reste stable | +| **Homonymes** (« Between two worlds ») | une œuvre en trop se voit et s'ignore, contrairement à une œuvre manquante. Demanderait de garder les dates de vie du `dc:creator` | +| **OpenLibrary en second rideau** | la justification est retombée (voir « Une source muette n'est pas une bibliographie vide ») ; les œuvres remonteraient en langue originale, donc non rapprochables par `CleOeuvre` | +| **EAN-2 confirmé en kiosque** | demande un magazine réel devant la caméra : rien de ce qui se fait au clavier ne le remplace | + +### Vérifié en exécution le 2026-08-21 + +API lancée sur une base neuve, migration `ImagesEtUnesDesNumeros` appliquée au démarrage : + +| Cas | Résultat | +|---|---| +| `POST /api/revues` avec `24666718` | fiche créée, `issn` = **`2466-6718`** | +| `POST /api/revues` avec `2466-6718` | **même fiche** (id 1), aucune seconde revue | +| `POST …/numeros` avec couverture et deux articles | les deux ressortent dans `NumeroRevueDto` | +| `PUT /api/revues/numeros/1` | la une est **remplacée**, la note apprise après coup | +| `GET /api/couvertures?url=…` (couverture de **numéro**) | **200 image/jpeg, 43 921 o** | +| `GET /api/couvertures?url=…` (URL inconnue) | 404 | + +**506 tests au vert** (479 avant ce lot). + +⚠️ **Ce qui n'a PAS été vérifié en navigateur** : les écrans. Le rendu de la fiche revue, du +scan depuis une série ou depuis les envies, du « Tout cocher » et de l'indicateur d'attente +repose sur la compilation et la relecture, pas sur une exécution. À regarder au prochain +passage sur un vrai appareil. diff --git a/IDEES.md b/IDEES.md index 0b16ccf..e60d3fc 100644 --- a/IDEES.md +++ b/IDEES.md @@ -268,6 +268,11 @@ Non actés : ce fichier ne fait pas autorité, `CLAUDE.md` reste la référence. ## Lot I — La bibliographie d'un auteur +✅ **Traité le 2026-08-21** : voir `CLAUDE.md`, section « Lots I à P ». « Tout cocher » vit à +côté du filtre (la barre de sélection n'apparaît qu'une fois une case cochée, trop tard pour +rendre service) et ne coche que ce qui est **visible** ; la couverture d'une œuvre non possédée +n'est chargée qu'au dépliage. + - **I1. « Tout cocher », à côté de « Tout décocher »**. Dès qu'une case est cochée, la barre de sélection n'offre que le geste inverse : pour ajouter aux envies l'essentiel d'une bibliographie, il faut cocher ligne à ligne. ⚠️ « Tout » doit signifier **ce qui est @@ -283,6 +288,10 @@ Non actés : ce fichier ne fait pas autorité, `CLAUDE.md` reste la référence. ## Lot J — Le scan là où il manque +✅ **Traité le 2026-08-21.** Les deux items étaient du raccordement, `ScannerCodeBarres` étant +déjà autonome. Pour J1, le scanner s'ouvre **dans la place visée** : il n'y a donc pas de retour +à organiser, on n'a jamais quitté la série. + - **J1. Scanner depuis une série**, sur une place vide. Le flux existe déjà en pièces détachées (`/ajout` décode, la place sait accueillir un livre créé par ISBN), mais il faut passer par l'écran d'ajout puis revenir rattacher à la main. ⚠️ Le retour doit ramener **sur la série et @@ -294,6 +303,13 @@ Non actés : ce fichier ne fait pas autorité, `CLAUDE.md` reste la référence. ## Lot K — Couvertures et saisie +**K2 traité le 2026-08-21** ; **K1 écarté avec l'utilisateur** au profit d'une URL collée (voir +le lot O1) — le stockage de fichiers reste hors du projet. + +⚠️ K2 est allé un cran plus loin que ce qui est écrit ci-dessous : la forme à tiret est aussi +celle **rangée en base**, faute de quoi un ISSN saisi à la main ne se rapprochait ni du +code-barres ni de `bib.issn`. Les champs de saisie, eux, gardent bien la valeur tapée. + - **K1. Photographier la couverture** quand aucune source n'en fournit. ⚠️ C'est le premier point du projet qui **stocke un fichier**, ce que « Ebooks : fiches uniquement » écartait jusqu'ici : espace disque YunoHost, sauvegarde plus lourde, et cache hors-ligne à alimenter @@ -310,6 +326,10 @@ Non actés : ce fichier ne fait pas autorité, `CLAUDE.md` reste la référence. ## Lot L — Rendre l'attente visible +✅ **Traité le 2026-08-21** : composant `Patience`, posé sur le lookup ISBN, les recherches BnF, +la bibliographie et les nouveautés. ⚠️ Il ne remplace aucun message d'échec — une source muette +garde ses `EtatSourceBibliographie`. + - **L1. Un indicateur pendant les appels lents**, faute de quoi une recherche BnF (≈1 s par page, jusqu'à dix pages pour les nouveautés) se lit comme une application plantée. ⚠️ Le besoin n'est pas décoratif : le lot H a montré qu'un écran qui ne dit rien pousse à recliquer, @@ -320,6 +340,10 @@ Non actés : ce fichier ne fait pas autorité, `CLAUDE.md` reste la référence. ## Lot M — Détails d'écran +✅ **Traité le 2026-08-21.** ⚠️ Le point qui fait tenir la ligne à 320 px n'est pas le `flex` +mais `min-width: 0` sur le champ : sans lui, un élément de formulaire refuse de rétrécir sous sa +largeur intrinsèque et pousse le bouton hors de l'écran. + - **M1. Séries : « Nom d'une série » et le bouton « Créer » sur la même ligne.** Le champ et son bouton s'empilent aujourd'hui. ⚠️ À 320 px, la ligne doit rester lisible : le champ prend la place restante, le bouton garde sa largeur propre — et le libellé ne se tronque pas. @@ -361,6 +385,11 @@ Pistes, dans l'ordre où elles se testent : ## Lot O — Les numéros de revue s'enrichissent +✅ **O1 et O2 traités le 2026-08-21** : URL collée (pas de photo), table `ArticleUne` à part, +séparateur point-virgule — et les thèmes de livres s'y alignent, la virgule restant acceptée. +⚠️ Le garde du relais `GET /api/couvertures` a bien été étendu à `NumerosRevue.CoverUrl` : +l'oublier n'aurait produit aucune erreur visible, seulement une image absente hors-ligne. + - **O1. Une image par numéro de revue.** `NumeroRevue` n'a aujourd'hui aucune couverture, et aucune source ne peut en fournir : l'ISSN désigne la revue, pas la parution. L'image viendra donc d'une photo ou d'une URL collée. ⚠️ **Même question ouverte que K1** — si c'est une @@ -387,15 +416,19 @@ Pistes, dans l'ordre où elles se testent : semble la bonne**, mais c'est une décision à acter, pas un détail d'implémentation. ✅ **Séparateur tranché le 2026-08-20 : le point-virgule.** Un titre d'article contient souvent - une virgule (« Ukraine, deux ans après »), qui couperait le titre en deux. ⚠️ **Reste une - question ouverte** : les thèmes de livres se saisissent à la virgule — faut-il les aligner sur - `;` pour que deux champs voisins ne se saisissent pas différemment, ou assumer la divergence - parce qu'un thème (« dark fantasy ») ne contient jamais de virgule ? À poser à l'utilisateur - avant de coder. Le reste suit les règles déjà tenues : dédoublonnage avant résolution, `NormalisationTexte`, + une virgule (« Ukraine, deux ans après »), qui couperait le titre en deux. ✅ **Question tranchée avec + l'utilisateur le 2026-08-21 : les thèmes s'alignent sur `;`**, la virgule restant acceptée en + repli — un thème n'en contient jamais, et c'était l'habitude. Le champ des auteurs se saisit + déjà au point-virgule : trois champs voisins du même formulaire ne doivent pas se saisir de + trois façons. Le reste suit les règles déjà tenues : dédoublonnage avant résolution, `NormalisationTexte`, premier libellé saisi = forme affichée. ## Lot P — Ranger une envie dans une série +✅ **Traité le 2026-08-21**, par la seconde voie du tableau ci-dessous et **depuis la série** +(tranché avec l'utilisateur : c'est là qu'on voit l'ordre de lecture, donc là qu'on sait quelle +position donner). L'envie survit au rattachement, et rien ne relie les deux en base. + Demande : depuis la liste d'envies, placer un livre souhaité dans une série, comme on y place un livre du catalogue. C'est le **chemin inverse** de `MettreEnEnviesAsync`, qui fait déjà « place vide → envie ».