# CLAUDE.md — Contexte projet MaBibli Ce fichier donne le contexte complet du projet à Claude Code. Lis-le en entier avant de commencer à coder. ## 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 | Déléguée au SSO de YunoHost | Pas de système de login custom à maintenir | | 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 `true` 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 ``/`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.POSSIBLE_FORMATS] = new List { 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 — à trancher avant de coder **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 à OpenLibrary. ⚠️ **Point d'architecture non résolu** : « 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. Le service worker de la PWA met en cache les *assets* (HTML/CSS/WASM), mais **pas les réponses de l'API**. Sans travail supplémentaire, l'app se lancera hors-ligne mais affichera une liste vide. Options à évaluer : 1. **Cache client des réponses API** (IndexedDB, ou cache du service worker sur les routes `GET`) — le plus simple, lecture seule, suffisant pour le besoin exprimé. 2. **SQLite compilé en WASM côté client**, avec persistance IndexedDB/OPFS, synchronisé avec le serveur — bien plus lourd, ne se justifie que si l'écriture hors-ligne devient nécessaire. Par défaut, partir sur l'option 1 tant que le besoin reste la consultation. ## 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) : 1. **OpenLibrary** (`https://openlibrary.org/isbn/{isbn}.json`) — source principale, gratuite, sans clé - ⚠️ **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) 2. **Fallback envisageable** : autres sources ouvertes (à évaluer — éviter Google Books sauf si l'utilisateur change d'avis explicitement) ## 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 └── UtilisateurId (propriétaire, via SSO YunoHost) 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. ## 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 - **Mécanisme exact du SSO YunoHost** : le principe est acté (pas d'auth custom), mais l'implémentation reste à préciser — vraisemblablement la lecture d'en-têtes injectés par SSOwat derrière nginx (`Remote-User`). Cela conditionne le remplissage de `UtilisateurId`. - **Sources ISBN de fallback** : toujours « à évaluer ». Candidats sans clé API et hors Google : OpenLibrary Search API, BnF (SRU — pertinent pour le fonds francophone), Wikidata. - **Choix du cache hors-ligne** : voir la section dédiée.