# 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 |
| Statut de lecture | **Par utilisateur**, pas commun (décidé le 2026-08-17, après la phase 3) | Le livre est commun, sa lecture est personnelle : deux membres du foyer lisent le même exemplaire à des rythmes différents. Sort `Statut` de `Livre` vers une table dédiée |
| Prêts | **Communs au foyer**, jamais filtrés par utilisateur ; un seul prêt ouvert par livre, garanti par index unique partiel | Un livre absent l'est pour tout le monde, et n'importe qui doit pouvoir noter son retour. Voir « Prêts » |
| Auteurs | **Table dédiée** avec nom normalisé, remplaçant le champ texte libre | Nécessaire au regroupement par auteur et à la recherche insensible aux accents. Regroupement **automatique seulement quand c'est sûr**, sinon proposé à l'utilisateur |
| Liste d'envies | **Table dédiée `LivreSouhaite`**, personnelle (par `YNH_USER`), jamais dans `Livres` | Un livre souhaité n'est pas possédé. Dans `Livres`, il entrerait dans le catalogue, les compteurs et les prêts, et il faudrait répéter « et qui n'est pas souhaité » à chaque lecture. Voir « Liste d'envies » |
| Bibliographie par auteur | **SRU BnF, index `bib.author`**, avec post-filtre obligatoire sur l'auteur réel de la notice | Vérifié le 2026-08-18. `all` rapproche les mots sur l'ensemble des auteurs d'une notice : sans post-filtre, « Émile Zola » remonte l'œuvre de sa fille. Voir « Bibliographie par auteur » |
| Export de la liste d'envies | **`.txt` et `.csv`**, produits côté serveur, sans dépendance | Les deux usages d'IDEES.md diffèrent : le texte s'emporte en librairie, le CSV s'ouvre dans un tableur. Voir « Export » |
| 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 | **zbar** (LGPL-2.1) compilé en WebAssembly, via les assets du paquet `ZBar.Blazor` ; notre JS garde la caméra | ZXing.Net **ne lisait pas** des codes-barres que zbar lit sur le même livre (2026-08-19). Plus léger de surcroît. Voir la section dédiée ci-dessous |
| Consultation hors-ligne | **Instantanés JSON en IndexedDB**, lecture seule, implémenté le 2026-08-18 | 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 — zbar (décision actée le 2026-08-19)
Le décodage EAN-13 se fait par **[zbar](https://github.com/mchehab/zbar) compilé en
WebAssembly**, appelé depuis `wwwroot/js/scanner-camera.js`. Les assets viennent du paquet
NuGet [`ZBar.Blazor`](https://www.nuget.org/packages/ZBar.Blazor) (LGPL-2.1).
### Pourquoi ZXing.Net a été abandonné
**Constat d'usage** : l'ISBN `9782846391009` n'était pas décodé par l'application, alors qu'il
l'était par une application d'essai utilisant zbar, **sur le même livre et le même appareil**.
⚠️ **Le raisonnement qui avait écarté cette piste était faux, et la faute est instructive.**
Ce fichier concluait que « le décodeur n'y est pour rien » à partir de mesures de **vitesse**
(4-6 ms/frame en WASM interprété). Or **la vitesse ne dit rien du taux de réussite** : un
décodeur peut être rapide et rater. Les mesures ne pouvaient pas soutenir cette conclusion.
Ne jamais reconduire ce raccourci : pour juger un décodeur, il faut mesurer **ce qu'il lit**,
pas ce qu'il coûte.
### Ce qui est établi, et ce qui ne l'est pas
**Établi** : zbar lit ce code-barres, ZXing.Net ne le lit pas, dans les mêmes conditions.
**Non établi** : que le moteur soit *seul* en cause. L'ancienne implémentation ne faisait pas que
décoder, elle **recadrait** sur la bande centrale (45 % de la hauteur) après réduction à 640 px,
pour limiter ce qui traversait le pont JS→C#. Ce recadrage était la seconde cause possible.
La bascule l'a supprimé — l'image entière est désormais analysée, à 960 px de large — donc la
question ne se pose plus.
### Ce que la bascule change dans le code
| | Avant (ZXing.Net) | Après (zbar) |
|---|---|---|
| Décodage | C#, `IsbnScanner.TryDecode` | JS, `window.zbar.scanImageData` |
| Pixels sur le pont JS→C# | **un `byte[]` par frame** | aucun — seule la valeur décodée passe |
| Zone analysée | bande centrale, 640 px | image entière, 960 px |
| Caméra | `scanner-camera.js` | `scanner-camera.js`, **inchangé** |
`IsbnScanner.cs`, `BancEssaiScan.cs` et `IsbnScannerTests.cs` ont disparu.
⚠️ **Coût assumé : six tests de décodage en moins.** Ils s'exécutaient en C# sur des images
générées ; le décodeur étant maintenant en JS, ils n'ont plus d'équivalent dans xUnit. Le
décodage a été vérifié dans le navigateur, sur un EAN-13 rendu en canvas — `9782846391009`
ressort bien en `ZBAR_EAN13`. C'est une vérification, pas un garde-fou permanent.
### ⚠️ On n'utilise PAS le composant `ZBarCamera` du paquet — et il ne faut pas y revenir
Son `camera.js` ouvre la caméra lui-même et **avale les erreurs** :
```js
navigator.mediaDevices.getUserMedia(constraints).then(...).catch(function (error) {
console.log(error); // et c'est tout
});
```
On perdrait d'un coup :
- les messages qui distinguent **permission refusée**, **aucune caméra**, **caméra occupée** et
**contexte non sécurisé** — ce que ce fichier documente comme durement acquis (une
`DOMException` perd son `name` en traversant le pont, d'où les codes de statut) ;
- `facingMode: { ideal: 'environment' }`, donc la **caméra arrière** sur téléphone : le composant
demande `{ video: true }`, c'est-à-dire la caméra frontale par défaut ;
- l'indication de résolution `1280×720`.
Le paquet est donc référencé **pour ses assets statiques seulement** (`zbar.js`, `zbar.wasm`).
Vérifié après bascule : caméra refusée → « L'accès à la caméra a été refusé… », message intact.
### `zbar.wasm` est chargé à la demande, pas au démarrage
`scanner-camera.js` injecte `_content/ZBar.Blazor/zbar.js` **à la première ouverture du
scanner**. Personne ne télécharge 139 Ko pour consulter sa bibliothèque.
⚠️ `zbar.js` **n'est pas un module ES** : il pose `window.zbar`. D'où l'injection d'une balise
`` vide, `src="_framework/blazor.webassembly#[.{fingerprint}].js"` littéral), alors que seuls les noms empreintés existaient sur disque. La racine répondait **200**, le script de démarrage **404**, l'application publiée restait **blanche**. `dotnet publish MaBibli.Client` seul, lui, produisait un `index.html` correct.
#### La cause réelle (SDK .NET 10.0.300)
Elle est dans `Microsoft.NET.Sdk.StaticWebAssets.HtmlAssetPlaceholders.targets`. La cible de **publication** `GenerateHtmlAssetPlaceholdersPublishStaticWebAssets` reçoit la liste des fichiers HTML à réécrire via `HtmlFiles="@(_HtmlStaticWebAssets)"` — or cet item n'est **jamais alimenté par le chemin de publication** : il l'est uniquement par la cible de **build** `ResolveHtmlAssetPlaceholdersBuildConfiguration`.
Quand on publie le client seul, le build a tourné dans la même instance MSBuild, l'item est rempli, la réécriture a lieu. Quand c'est l'**API** qui publie, elle demande au projet client ses assets de publication dans une instance où la cible de build n'a **pas** tourné : `@(_HtmlStaticWebAssets)` est vide, la tâche de réécriture ne produit **aucun fichier**, aucun asset HTML calculé n'entre dans `staticwebassets.publish.json` — et c'est alors le fichier source `MaBibli.Client/wwwroot/index.html` qui est recopié tel quel par `ComputeResolvedFilesToPublishList`, placeholders compris. Vérifié : le manifeste de publication de l'API ne contient **aucun** asset `.html`.
C'est pour cela que `true` sur l'API ne changeait rien : la cible s'exécutait bien, mais sur une liste vide.
#### La correction retenue
Supprimer le besoin de réécriture plutôt que réparer la réécriture : sans import map ni placeholder, il n'y a plus rien à substituer, et le fichier source recopié tel quel est déjà le bon.
- `false` sur le client ;
- **pas** de `OverrideHtmlAssetPlaceholders` (c'est lui qui, à `true`, force `BlazorFingerprintBlazorJs` et empreinte `blazor.webassembly.js` — la tentative précédente n'avait échoué que parce que les deux propriétés étaient combinées) ;
- `index.html` : `