Documente les lots I à P, et ce qui reste volontairement de côté

Les décisions, les pièges, et ce qui n'a pas été vérifié : les écrans reposent
sur la compilation et la relecture, pas sur une exécution en navigateur.

Restent écartés, avec leur motif : l'allègement des couvertures (à mesurer
d'abord, même règle que l'AOT), la photo de couverture (le projet ne stocke pas
de fichiers), l'URL finale après redirections, les homonymes d'auteur, et la
confirmation de l'EAN-2, qui demande un magazine devant la caméra.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mathieu
2026-08-21 11:25:58 +02:00
co-authored by Claude Opus 5
parent 7c050cb410
commit 41e5c7a3fb
2 changed files with 229 additions and 5 deletions
+191
View File
@@ -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.
+38 -5
View File
@@ -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 ».