Files
mabibli/CLAUDE.md
T
mathieuandClaude Opus 5 a314dcfc79 Recueillir les idees d'interface dans IDEES.md
Retours d'usage a traiter plus tard : agrandissement de la couverture au
clic, statut de lecture en lecture seule dans la liste et remonte au
niveau du type, affichage du format limite aux ebooks (physique par
defaut), fiche en lecture seule avec bouton d'edition, regroupement des
livres par auteur.

Documente aussi un cas de donnee sale mesure sur l'ISBN 9782749942636 :
la notice BnF colle le nom de l'auteur en fin de titre sans separateur
ISBD, donc la regle de nettoyage existante ne peut pas s'appliquer. Ce
n'est ni un doublon ni un defaut du parsing.

Garde en fin de fichier l'idee des livres souhaites (bibliographie par
auteur avec ouvrages possedes grises), hors perimetre v1, avec ses
implications sur le modele de donnees.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:16:16 +02:00

310 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md — Contexte projet MaBibli
Ce fichier donne le contexte complet du projet à Claude Code. Lis-le en entier avant de commencer à coder.
Voir aussi **`IDEES.md`** : améliorations identifiées mais **non encore actées**. Rien n'y fait autorité — ce fichier-ci reste la référence.
## Objectif du projet
Application self-hosted de gestion de bibliothèque personnelle, à héberger sur YunoHost, accessible depuis smartphone et PC.
## Fonctionnalités attendues (v1)
1. **Catalogue de livres physiques** — liste, ajout, édition, suppression
2. **Catalogue de livres numériques (ebooks)** — même chose, avec un champ format distinct du physique
3. **Gestion de prêts**
- Prêter un livre à une personne (nom, date de prêt)
- Marquer comme récupéré (date de retour)
- Historique des prêts passés par livre (pas juste l'état courant)
4. **Récupération automatique des infos via ISBN**
- Scan caméra du code-barres (EAN-13 / ISBN)
- Saisie manuelle de l'ISBN
- Dans les deux cas : appel à une ou plusieurs API pour pré-remplir titre, auteur, éditeur, couverture
5. **Statuts de lecture** — à lire / en cours / lu (au minimum), assignable à chaque livre
## Décisions techniques actées
| Sujet | Décision | Pourquoi |
|---|---|---|
| Langage backend | C# / ASP.NET Core | Choix de l'utilisateur, typage fort |
| Frontend | Blazor WebAssembly | Support PWA quasi natif (`dotnet new blazorwasm --pwa`), tout en C#, offline partiel |
| Base de données | SQLite + Entity Framework Core | Fichier unique, pas de serveur DB séparé, adapté à un usage perso/familial |
| Auth / multi-utilisateur | SSO YunoHost via **en-têtes SSOwat** (`YNH_USER`) | Pas de login custom. OIDC **écarté** : non documenté par YunoHost (vérifié le 2026-08-17). Voir « Intégration SSO » |
| Portée des données | **Collection commune** à tous les utilisateurs, avec traçabilité de qui a ajouté chaque livre | Usage familial : une bibliothèque de foyer, pas des collections étanches. Laisse la possibilité de cloisonner plus tard sans migration lourde |
| Ebooks | **Fiches uniquement**, pas de stockage de fichiers | Inventaire, pas hébergement. Évite l'espace disque YunoHost, les sauvegardes lourdes, et garde le cache hors-ligne léger |
| Architecture serveur | **x86_64** → publish `linux-x64` | Serveur PC/VPS confirmé par l'utilisateur |
| Production des binaires | **Compilation locale + release manuelle**, via un script réutilisable en CI plus tard | Ne pas se bloquer sur l'outillage ; Gitea Actions nécessiterait un runner, non vérifié |
| Cache hors-ligne | **IndexedDB** (pas le cache du service worker) | Seule option permettant recherche et tri hors-ligne sur toute la bibliothèque, et l'affichage de la date de dernière synchro |
| Notices BnF multiples | **Demander systématiquement** à l'utilisateur | Exactitude de l'édition privilégiée sur la vitesse de saisie en série |
| AOT WebAssembly | **Désactivé par défaut**, à réévaluer après mesure | Le mode interprété devrait suffire (~5-10 ms/frame estimés) ; ne pas payer le coût de build avant d'avoir constaté un problème |
| Hébergement | YunoHost, installation **native** (pas Docker) | YunoHost déconseille Docker pour ses apps (moins fiable, plus lourd) ; installation native = meilleures perfs sur petit matériel |
| Packaging YunoHost | S'inspirer de [`radarr_ynh`](https://github.com/YunoHost-Apps/radarr_ynh) | Radarr est aussi en .NET, packagé sans Docker sur YunoHost. Leur `manifest.toml` montre un déploiement **self-contained** (`dotnet publish -r linux-x64 --self-contained`), donc pas besoin d'installer `dotnet-runtime` via apt côté serveur — le binaire embarque son propre runtime |
| Scan ISBN | **ZXing.Net** (C#, Apache 2.0) exécuté dans le WASM ; le JS ne fournit que les pixels caméra | Décodage en C#, réutilisable hors navigateur si le projet évolue en scanner de bibliothèque. Voir la section dédiée ci-dessous |
| Consultation hors-ligne | Cache local des données côté client (mécanisme à trancher) | Le besoin est de **consulter la bibliothèque existante** sans réseau, pas d'enrichir de nouveaux livres. Voir « Stratégie hors-ligne » |
## Scan du code-barres — ZXing.Net (décision actée)
Le décodage EAN-13 se fait en **C# avec [ZXing.Net](https://www.nuget.org/packages/ZXing.Net)** (`micjahn`, Apache 2.0), et non avec une bibliothèque JS type `html5-qrcode`.
### Pourquoi
- **Réutilisable hors navigateur.** Si le projet évolue vers un scanner de bibliothèque (app native, scan en masse, décodage d'une photo côté serveur), le code de décodage se transpose tel quel. Une bibliothèque JS serait à réécrire intégralement.
- **Un seul langage**, cohérent avec le reste de la stack.
- L'argument « offline » n'entre **pas** en compte ici : `html5-qrcode` fonctionne aussi hors-ligne (fichier JS servi par la PWA, aucun appel réseau). Ce n'est pas un critère de départage.
### Mesures réelles (validées sur .NET 10, publish Blazor WASM OK)
| Mesure | Résultat |
|---|---|
| Décodage EAN-13 propre (380×160) | 0,04 ms/frame |
| **Pire cas** : frame 640×480 bruitée sans code-barres (échec) | 0,53 ms/frame |
| Surcoût du payload PWA | +192 Ko (brotli) |
Le pire cas est le chiffre qui gouverne le framerate : la majorité des frames caméra ne contiennent pas de code-barres lisible, et c'est l'échec de décodage qui coûte le plus cher.
⚠️ Ces chiffres sont mesurés en **JIT x64 natif**. En Blazor WASM le code est *interprété* par défaut : compter un facteur ~10-20×, soit ~5-10 ms/frame — largement suffisant pour scanner à 10-15 fps. Activer `<RunAOTCompilation>true</RunAOTCompilation>` ramène ça à 1-2 ms, au prix d'un build nettement plus lent.
### Pièges à connaître
- **Le JS interop ne disparaît pas.** `getUserMedia` et `<canvas>`/`getImageData` sont des API web sans équivalent C#. Prévoir ~30 lignes de JS maison dont le seul rôle est de pousser un `byte[]` vers C#. Toute la logique de décodage reste en C#.
- **Ne pas perdre de temps à essayer de réduire la taille via un reader ciblé.** Remplacer `MultiFormatReader` par `EAN13Reader` pour aider le trimmer **ne change rien** : mesuré à 192 495 octets à l'octet près dans les deux cas. ZXing.Net n'est pas trim-friendly.
- `RGBLuminanceSource` accepte directement le buffer RGBA du canvas (`BitmapFormat.RGBA32`) — **aucune bibliothèque d'image nécessaire** (pas de SkiaSharp ni ImageSharp).
- Le scan caméra exige **HTTPS** (garanti par YunoHost en prod ; en dev, `localhost` est considéré comme sûr).
### Squelette validé
```csharp
using ZXing;
using ZXing.Common;
public static class IsbnScanner
{
static readonly MultiFormatReader Reader = new()
{
Hints = new Dictionary<DecodeHintType, object>
{
[DecodeHintType.POSSIBLE_FORMATS] = new List<BarcodeFormat>
{
BarcodeFormat.EAN_13, BarcodeFormat.EAN_8,
},
[DecodeHintType.TRY_HARDER] = true,
},
};
/// rgba : buffer brut issu de ctx.getImageData(...).data
public static string? TryDecode(byte[] rgba, int width, int height)
{
var source = new RGBLuminanceSource(
rgba, width, height, RGBLuminanceSource.BitmapFormat.RGBA32);
return Reader.decode(new BinaryBitmap(new HybridBinarizer(source)))?.Text;
}
}
```
## Stratégie hors-ligne — décidé : consultation seule
**Besoin réel** : consulter la bibliothèque **déjà enregistrée** sans réseau (liste des livres, statuts, prêts en cours). Il ne s'agit **pas** d'enrichir de nouveaux livres hors-ligne — le lookup ISBN exige de toute façon un accès réseau.
⚠️ **Piège à ne pas sous-estimer** : « les données sont déjà en local » n'est vrai qu'au sens *serveur*. En Blazor WebAssembly, le code tourne dans le navigateur, alors que SQLite vit côté serveur YunoHost. Le service worker de la PWA met en cache les *assets* (HTML/CSS/WASM), mais **pas les réponses de l'API**. Sans travail explicite, l'app se lancera hors-ligne et affichera **une liste vide**.
**Décision** : cache client des réponses `GET` de l'API (IndexedDB, ou cache du service worker), en **lecture seule**.
- Pas de file d'attente d'écritures, pas de synchronisation, pas de résolution de conflits.
- Hors-ligne, l'interface doit **désactiver explicitement** les actions d'écriture (ajout, édition, prêt) plutôt que de les laisser échouer silencieusement, et indiquer que les données affichées proviennent du cache.
- SQLite compilé en WASM côté client a été **écarté** : ne se justifierait que si l'écriture hors-ligne devenait nécessaire.
## Sources de données ISBN — point d'attention important
**Ne pas dépendre d'une seule source, et éviter Google Books si possible** (préférence explicite de l'utilisateur : pas de dépendance à Google).
Stratégie : interroger plusieurs sources **libres et gratuites, sans clé API obligatoire**, en cascade (si la première ne répond pas ou renvoie des données incomplètes, essayer la suivante).
**Ordre décidé : BnF d'abord, OpenLibrary ensuite.** La collection est majoritairement francophone, or OpenLibrary (Internet Archive) est très fourni sur l'édition anglophone mais **lacunaire sur le fonds français** — éditions françaises récentes et poches d'éditeurs modestes y manquent souvent, ou n'ont qu'un titre sans auteur. Une source unique lacunaire ruinerait l'intérêt du scan, qui est précisément d'éviter la saisie manuelle.
1. **BnF** — source principale, via son API **SRU**, gratuite et sans clé. Le dépôt légal français garantit structurellement la meilleure couverture possible sur le francophone. **API testée et validée le 2026-08-17** — voir la section « API BnF » ci-dessous pour les pièges, dont un bloquant.
2. **OpenLibrary** (`https://openlibrary.org/isbn/{isbn}.json`) — source de secours, pour les livres étrangers et tout ce que la BnF ne connaît pas
- ⚠️ **Bug connu identifié lors des tests avec BookLogr** : l'endpoint `/isbn/{isbn}.json` renvoie souvent le titre mais l'**auteur est juste une référence** (`/authors/OL...A`), pas le nom directement. Il faut faire un **second appel** vers `/authors/{id}.json` pour récupérer le nom. Un projet qui oublie ce second appel se retrouve avec titre rempli mais auteur vide (symptôme exact observé et diagnostiqué chez BookLogr) — **ne pas reproduire ce bug**.
- La couverture est un service séparé : `https://covers.openlibrary.org/b/isbn/{isbn}-L.jpg` (peut exister même si la fiche bibliographique est incomplète)
3. **Fallback ultérieur envisageable** : Wikidata. Ne pas l'implémenter tant que la cascade BnF → OpenLibrary n'a pas montré ses limites en usage réel.
**Google Books reste écarté** conformément à la préférence de l'utilisateur, sauf changement d'avis explicite de sa part.
Quelle que soit la source, prévoir que le formulaire de saisie manuelle reste **toujours accessible** pour compléter ou corriger une fiche incomplète.
## API BnF — validée le 2026-08-17
Endpoint SRU, gratuit, **sans clé API**, réponse en ~240 ms :
```
https://catalogue.bnf.fr/api/SRU
?version=1.2
&operation=searchRetrieve
&query=bib.isbn all "{isbn}"
&recordSchema=dublincore
&maximumRecords=5
```
**Utiliser `recordSchema=dublincore`, pas MARC.** Le Dublin Core renvoie directement `dc:title`, `dc:creator`, `dc:publisher`, `dc:date`, `dc:language`, `dc:format` — bien plus simple à mapper que l'UNIMARC. Le nombre de résultats se lit dans `<srw:numberOfRecords>`.
### ⚠️ Piège bloquant : ISBN-13 vs ISBN-10
**La BnF indexe l'ISBN tel qu'imprimé sur le livre.** Les ouvrages publiés avant 2007 ne portent qu'un **ISBN-10** et sont donc **introuvables par leur ISBN-13**, alors que le scanner de code-barres lit toujours un EAN-13.
Mesuré sur un échantillon de livres français dont l'existence a été confirmée via OpenLibrary :
| ISBN-13 | Recherche ISBN-13 | Recherche ISBN-10 |
|---|---|---|
| 9782070612758 (Le Petit Prince, 2007) | **1 notice** | 0 |
| 9782253004226 (Germinal, Livre de poche) | 0 | **3 notices** |
| 9782080704092 (Le Horla, Flammarion) | 0 | **1 notice** |
Sans conversion, on perd la majorité du fonds ancien — précisément les livres d'une bibliothèque constituée. **Toujours interroger les deux formes** : ISBN-13 d'abord, puis ISBN-10 converti si aucun résultat.
Conversion ISBN-13 → ISBN-10 (uniquement pour le préfixe `978`) : retirer `978`, garder les 9 chiffres, recalculer la clé (somme pondérée 10→2, modulo 11, `X` si le reste vaut 10).
### Autres pièges confirmés
- **Pas de couverture.** Le Dublin Core BnF n'en fournit aucune. Utiliser OpenLibrary pour l'image, **quelle que soit la source des métadonnées** : `https://covers.openlibrary.org/b/isbn/{isbn}-L.jpg?default=false`. Le `?default=false` est **indispensable** — sans lui, OpenLibrary renvoie une image placeholder au lieu d'un 404. Testé : disponible pour les 4 ISBN de l'échantillon, y compris ceux absents d'OpenLibrary côté bibliographique.
- **Plusieurs notices pour un même ISBN.** Germinal en renvoie 3 (rééditions successives partageant l'ISBN, éditeurs et années différents). Ne pas prendre aveuglément la première : soit proposer le choix à l'utilisateur, soit retenir la plus récente via `dc:date`. À trancher à l'implémentation.
- **Ponctuation ISBD à nettoyer.** Les champs ne sont pas exploitables bruts. Règles validées :
| Champ | Brut | Nettoyé |
|---|---|---|
| `dc:title` | `Germinal / Émile Zola ; préface d'Armand Lanoux` | `Germinal` — couper au premier ` / ` |
| `dc:creator` | `Zola, Émile (1840-1902). Auteur du texte` | `Émile Zola` — retirer les dates entre parenthèses, le rôle après le point, puis inverser `Nom, Prénom` |
| `dc:publisher` | `le Livre de poche (Paris)` | `le Livre de poche` — retirer la ville en fin de chaîne |
- **Couvertures OpenLibrary : 502 intermittents.** Mesuré en phase 2 — trois appels consécutifs sur le même ISBN ont donné 200, 200, puis 502. Ne **jamais** valider l'URL par un HEAD avant de l'afficher : on supprimerait au hasard des couvertures existantes. L'URL est émise systématiquement, et **l'interface doit gérer l'image cassée** (`onerror`), le `?default=false` garantissant un 404 franc plutôt qu'un placeholder silencieux.
- **OpenLibrary : le double appel auteur ne suffit pas toujours.** Cas réel (*Introduction to Algorithms*) : l'édition n'a aucun champ `authors`, seulement `contributions` et `by_statement`. Le repli implémenté passe par `/works/{id}.json` → clés d'auteurs → `/authors/{id}.json`, puis en dernier recours `by_statement`. Sans ce repli, l'auteur serait vide — c'est la même classe de bug que celui de BookLogr, sous une autre forme.
- **Livres étrangers absents**, comme attendu du dépôt légal français : *Introduction to Algorithms* et *Effective Java* introuvables sous les deux formes d'ISBN. C'est exactement le rôle d'OpenLibrary en second rideau — la cascade est donc bien nécessaire, pas seulement confortable.
## Intégration SSO YunoHost
**Mécanisme retenu : en-têtes HTTP injectés par SSOwat.** nginx authentifie le visiteur via le portail YunoHost, puis transmet l'identité à l'application dans des en-têtes que l'app se contente de lire :
| En-tête | Contenu |
|---|---|
| `YNH_USER` | nom d'utilisateur authentifié |
| `YNH_USER_EMAIL` | email |
| `YNH_USER_FULLNAME` | nom complet |
`YNH_USER_FULLNAME` évite une requête LDAP supplémentaire pour l'affichage.
**OIDC a été écarté** (vérifié le 2026-08-17) : la documentation de packaging YunoHost ne documente aucun fournisseur OpenID Connect pour les apps. Les seuls mécanismes officiels sont LDAP direct et ces en-têtes.
### Points de vigilance
- La doc YunoHost indique que ces en-têtes sont **protégés contre l'injection depuis le client** (SSOwat les écrase). Malgré cela, faire écouter le service .NET **uniquement sur `127.0.0.1`**, jamais sur `0.0.0.0` — défense en profondeur, à traiter comme une contrainte dure dans `systemd.service` et `nginx.conf`. Un service exposé directement sur le réseau permettrait de forger `YNH_USER` et de contourner tout le portail.
- **Limite connue** : se déconnecter du portail YunoHost ne déconnecte pas des apps, chacune conservant sa propre session/cookie.
- En développement local, il n'y a pas de SSOwat : prévoir un utilisateur simulé (en-tête forcé ou configuration de dev) plutôt que de désactiver l'auth.
## Architecture de déploiement YunoHost
### Trois projets .NET, un seul service
Le client Blazor WebAssembly n'est **pas** un serveur : compilé, ce ne sont que des fichiers statiques (HTML/CSS/`.wasm`) **servis par l'API**. Un seul processus tourne donc sur le serveur.
```
DÉVELOPPEMENT COMPILATION DÉPLOIEMENT
MaBibli.Client ─┐
MaBibli.Shared ─┼──► dotnet publish ──► un dossier ──► un service systemd
MaBibli.Api ─┘ MaBibli.Api unique sur 127.0.0.1:PORT
```
`MaBibli.Api` référence `MaBibli.Client` ; à la compilation, les fichiers du client atterrissent dans le `wwwroot` de l'API.
Commande de publication cible :
```
dotnet publish MaBibli.Api -c Release -r linux-x64 --self-contained
```
Le self-contained embarque le runtime .NET dans le binaire : **aucun `dotnet-runtime` à installer** côté serveur, pas de conflit de versions. C'est le modèle de `radarr_ynh`.
### Deux dépôts distincts
| Dépôt | Contenu | Rôle |
|---|---|---|
| `mabibli` | Le code C#, les 3 projets | Ce qui est développé |
| `mabibli_ynh` | `manifest.toml`, scripts, conf nginx/systemd | Comment l'installer sur YunoHost |
Le paquet `_ynh` **ne contient aucun code C#** : il porte des instructions d'installation et une URL vers une archive compilée, avec son empreinte SHA256.
```
mabibli_ynh/
├── manifest.toml ← identité, version, URL du binaire + sha256
├── conf/
│ ├── systemd.service ← lancement du service, port
│ └── nginx.conf ← reverse proxy + intégration SSO
└── scripts/
├── install / remove
├── upgrade
└── backup / restore
```
### Chaîne de publication
Compilation **locale**, puis dépôt manuel de l'archive en release sur le Gitea de l'utilisateur, et mise à jour du `sha256` dans le `manifest.toml`. Prévoir un **script de build** encapsulant ces étapes, écrit pour être réutilisable tel quel dans une CI (Gitea Actions) si l'utilisateur bascule plus tard.
**Ne jamais compiler sur le serveur** à l'installation : cela imposerait le SDK .NET complet sur la machine YunoHost, pour une compilation lente — l'inverse exact de ce que permet le self-contained.
## Modèle de données (base de départ, à affiner)
```
Livre
├── Id
├── Isbn
├── Titre
├── Auteur
├── Editeur
├── Format : Physique | Numerique
├── Statut : ALire | EnCours | Lu
├── CoverUrl
├── DateAjout
└── AjoutePar (YNH_USER — traçabilité, PAS un cloisonnement)
Pret
├── Id
├── LivreId (FK vers Livre)
├── Emprunteur (nom, texte libre)
├── DatePret
└── DateRetour (nullable — NULL tant que non rendu)
```
Garder `Pret` comme table séparée (pas un champ sur `Livre`) pour conserver l'historique complet des prêts passés, pas juste l'état actuel.
**`AjoutePar` est une information, pas une frontière.** La bibliothèque est commune : ne **jamais** filtrer les requêtes de lecture sur ce champ. Il sert à savoir qui a saisi le livre (et implicitement à qui il appartient), pas à restreindre l'accès. Ce choix permet de basculer plus tard vers des bibliothèques cloisonnées sans migration de schéma.
**Les prêts ne concernent en pratique que les livres physiques** — les ebooks étant de simples fiches, il n'y a pas d'objet à prêter. `Emprunteur` reste un **texte libre**, sans lien avec les comptes YunoHost : on suit les prêts à des personnes extérieures au foyer, pas les échanges entre utilisateurs de l'app.
## Historique du projet (pourquoi ces choix)
L'utilisateur a testé deux solutions existantes avant de se lancer dans un projet custom :
- **uBiblio** (Docker, Python) — gère prêts + scan ISBN, mais dépend de Google Books (clé API requise) pour l'autofill, ce qui ne convient pas à l'utilisateur qui veut éviter cette dépendance
- **BookLogr** (Docker, Python) — utilise OpenLibrary nativement mais a le bug décrit ci-dessus (auteur non récupéré)
Conclusion : aucune des deux solutions existantes ne coche toutes les cases (prêts + pas de dépendance Google + autofill fiable) → développement d'une solution sur mesure.
## Prochaines étapes suggérées
1. Scaffolder le projet Blazor WebAssembly PWA (`dotnet new blazorwasm --pwa`) — **commande vérifiée valide en .NET 10**, l'option `--pwa` existe toujours
2. Ajouter le **projet API ASP.NET Core** : le client WASM tourne dans le navigateur, SQLite vit côté serveur — une API est indispensable, elle n'était pas explicitée dans la version initiale de ce document
3. Mettre en place le modèle EF Core + SQLite + migrations
4. Implémenter le service de lookup ISBN (OpenLibrary avec le double-appel titre+auteur)
5. CRUD livres (physique/numérique, statuts de lecture)
6. Gestion des prêts
7. Intégration scan caméra (**ZXing.Net** + interop caméra minimal)
8. Cache hors-ligne pour la consultation (voir « Stratégie hors-ligne »)
9. Packaging YunoHost (`manifest.toml`, `conf/systemd.service`, `conf/nginx.conf`, `scripts/install`) en s'inspirant de radarr_ynh
## Questions ouvertes
**Tous les choix structurants ont été tranchés le 2026-08-17** — voir le tableau des décisions. Le cadrage est clos, le développement peut commencer.
Points à réévaluer en cours de route, sans blocage :
- **AOT WASM** : mesurer le scan sur un vrai téléphone une fois fonctionnel. Activer `RunAOTCompilation` seulement si la fluidité est insuffisante.
- **Runner Gitea Actions** : à vérifier le jour où l'utilisateur voudra automatiser les releases.
- **Wikidata en 3ᵉ source ISBN** : uniquement si la cascade BnF → OpenLibrary montre ses limites en usage réel.