# 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` : `