- Hors-ligne : consultation seule via cache des reponses GET. SQLite WASM cote client ecarte. L'UI devra desactiver explicitement les ecritures hors-ligne plutot que de les laisser echouer. - Portee des donnees : collection commune au foyer. `UtilisateurId` (proprietaire) devient `AjoutePar` (tracabilite) — ne jamais filtrer les lectures dessus. - Sources ISBN : BnF (SRU) en principale, OpenLibrary en secours. Motif : OpenLibrary est lacunaire sur le fonds francais. API BnF pas encore testee, a valider avant implementation. - SSO : en-tetes SSOwat `YNH_USER` / `YNH_USER_EMAIL` / `YNH_USER_FULLNAME`. OIDC ecarte, non documente par YunoHost (verifie ce jour). Corrige au passage le nom d'en-tete, qui n'est pas `Remote-User`. - Ebooks : fiches uniquement, pas de stockage de fichiers. Les prets ne concernent donc que les livres physiques. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
206 lines
15 KiB
Markdown
206 lines
15 KiB
Markdown
# 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 | 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 |
|
||
| 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.
|
||
- ⚠️ Renvoie du **XML MARC**, nettement plus rébarbatif à parser que du JSON. Prévoir le travail de mapping en conséquence.
|
||
- ⚠️ **API non encore testée dans ce projet** — à valider concrètement (format exact, disponibilité, tolérance au débit) avant de s'engager sur l'implémentation.
|
||
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.
|
||
|
||
## 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.
|
||
|
||
## 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
|
||
|
||
Les grands choix structurants ont été tranchés le 2026-08-17 (offline, portée des données, sources ISBN, SSO, ebooks). Restent :
|
||
|
||
- **Validation concrète de l'API SRU de la BnF** — à faire avant d'écrire le service de lookup, puisqu'elle est désormais la source principale.
|
||
- **Support technique du cache hors-ligne** : IndexedDB ou cache du service worker sur les routes `GET`. À trancher au moment de l'implémenter, une fois les écrans connus.
|
||
- **Activation de l'AOT WASM** (`RunAOTCompilation`) : à décider après mesure du scan sur un vrai téléphone. Ne pas l'activer par défaut, le build devient nettement plus lent.
|