# CLAUDE.md — contexte projet MaBibli Il ne redit pas ce que la documentation dit déjà. Le *pourquoi* détaillé, avec ses mesures, est dans [`docs/architecture.md`](docs/architecture.md) — à lire avant de toucher au modèle, au hors-ligne ou au déploiement. | Document | Répond à | |---|---| | [`README.md`](README.md) | Qu'est-ce que c'est, comment je le lance ? | | [`docs/architecture.md`](docs/architecture.md) | Pourquoi le code est ainsi ? | | [`docs/installer.md`](docs/installer.md) | Comment je l'héberge ? | | [`docs/publier-une-version.md`](docs/publier-une-version.md) | Comment je sors une version ? | | `../mabibli_ynh/doc/ADMIN.md` | Où sont les données, qui y a accès ? | --- ## Objectif Bibliothèque personnelle auto-hébergée, pour un **foyer**, consultable au téléphone comme au PC. Une seule collection commune, des lectures personnelles. Deux solutions existantes ont été essayées avant de se lancer : **uBiblio** (dépend de Google Books, clé d'API obligatoire) et **BookLogr** (auteur non récupéré, faute du second appel `/authors/{id}.json` d'OpenLibrary). Aucune ne cochait *prêts + pas de Google + autofill fiable*. ## Périmètre de la V1 Tout ce qui suit est **implémenté et éprouvé**. La V1 est un travail de finition, pas d'ajout : ce qui n'est pas dans ce tableau n'y entre pas. | | | |---|---| | Catalogue | livres physiques et ebooks (**fiches uniquement**, aucun fichier hébergé), revues et numéros, séries et cycles, thèmes | | Prêts | à qui, depuis quand, historique complet ; **un seul prêt ouvert par livre** | | Enrichissement | scan EAN-13 (zbar en WebAssembly), douchette USB, saisie manuelle ; cascade **BnF → OpenLibrary** | | Lecture | statut personnel *à lire / en cours / lu* | | Envies | liste personnelle de livres et de revues, ordonnée à la main, exportable `.txt` et `.csv` | | Bibliographie | tout ce qu'un auteur a écrit (SRU BnF), possédé grisé, nouveautés | | Hors-ligne | PWA installable, **consultation seule** depuis neuf instantanés IndexedDB | | Déploiement | paquet YunoHost (voie de référence), image Docker (voie secondaire, **sans authentification**) | **Hors périmètre, délibérément** : hébergement de fichiers (ebooks, photos de couverture), écriture hors-ligne, authentification propre, cloisonnement par utilisateur, rôles ou lecture seule. --- ## Décisions techniques actées | Sujet | Décision | Pourquoi | |---|---|---| | Backend | C# / ASP.NET Core, .NET 10 | choix de l'utilisateur, typage fort | | Frontend | Blazor WebAssembly, en PWA | tout en C#, PWA quasi native | | Base | SQLite + EF Core | fichier unique, pas de serveur DB séparé | | Auth | SSO YunoHost par en-têtes SSOwat (`YNH_USER`) | OIDC écarté : YunoHost n'en documente aucun pour les apps | | Portée des données | collection **commune**, statuts et envies **personnels** | une bibliothèque de foyer ; voir « les trois portées » | | Sources ISBN | **BnF d'abord, OpenLibrary ensuite** | fonds francophone ; **Google Books écarté** (préférence explicite, clé d'API) | | Scan | **zbar** (LGPL-2.1) compilé en WASM, notre JS garde la caméra | ZXing.Net **ne lisait pas** des codes que zbar lit sur le même livre | | Hors-ligne | instantanés JSON en **IndexedDB**, lecture seule | un cache de réponses HTTP ne rendrait rien d'une recherche jamais tapée en ligne | | Ebooks | **fiches uniquement** | inventaire, pas hébergement | | Hébergement | YunoHost, installation **native** (pas Docker) | recommandation YunoHost, meilleures perfs sur petit matériel | | Binaires | `dotnet publish --self-contained`, compilation **locale**, **une archive par architecture** (`linux-x64` et `linux-arm64`) | rien à installer côté serveur, rien à compiler sur le serveur. ⚠️ Le RID désigne le **serveur** : la machine qui compile peut être de l'autre architecture, le publish est croisé | | AOT WebAssembly | **désactivé**, à réévaluer après mesure | ne pas payer le coût de build avant d'avoir constaté un problème | --- ## Les invariants à ne jamais casser Chacun a été payé au moins une fois. Le détail est dans [`docs/architecture.md`](docs/architecture.md) ; ce qui suit est la liste dure. 1. **Les trois portées.** *Commune* (catalogue, auteurs, séries, revues, thèmes, prêts) — on ne filtre **jamais** dessus. *Personnelle* (statut de lecture, envies, bibliographies masquées) — on filtre **toujours** sur l'appelant, et l'objet d'autrui est **introuvable** (404), jamais refusé (403). *Trace* (`AjoutePar`) — une information, pas une frontière. 2. **Cinq tables refusent de fusionner** : `Livre`, `Revue`, `Serie`, `LivreSouhaite`, `RevueSouhaitee`. Les fondre obligerait à écrire « et qui n'est pas une revue » à *chaque* lecture — un invariant qu'on réécrit partout finit par être oublié quelque part. L'ergonomie peut être regroupée ; le schéma, non. 3. **Le service n'écoute que sur `127.0.0.1`.** C'est une frontière de sécurité : `YNH_USER` n'est digne de confiance que parce que SSOwat l'écrase à chaque requête. Ne jamais élargir `ASPNETCORE_URLS`. 4. **Toute vue de l'API reçoit son instantané hors-ligne** (`CacheHorsLigne`, noms de magasins : `catalogue`, `auteurs`, `prets-en-cours`, `utilisateur`, `souhaits`, `series`, `revues`, `souhaits-revues`, `version`). Sans le sien, l'écran est mort sans réseau — et le symptôme (un 404 en plein écran) ne désigne pas la cause. 5. **Toute dimension de filtre s'ajoute aux deux implémentations** et au test qui les confronte : `MaBibli.Shared/Catalogue/FiltreLivres.cs` (serveur, sur `IQueryable`) et `MaBibli.Client/Services/FiltreLivresLocal.cs` (navigateur, sur des DTO). 6. **Toute écriture recalcule les colonnes normalisées** (`RecalculerFormes()`, porté par les entités de `MaBibli.Shared/Entites/`). Sept colonnes en dépendent, dont trois portent une unicité — et `ServiceRenormalisation` les rattrape au démarrage. 7. **Toute route nouvelle s'inscrit dans `RemonteeRoutes`.** Le repli existe pour qu'une route oubliée reste utilisable, pas pour dispenser de la ligne. 8. **Ne pas réactiver les empreintes WASM** (`WasmFingerprintAssets=false`). Publier l'API réécrivait mal `index.html` et l'application restait blanche. 9. **On grise, on ne masque jamais** ; hors-ligne on **désactive avec son motif**, on ne fait pas disparaître. Un bouton absent est indiscernable d'une fonction supprimée. 10. **On n'affiche que ce qui a été choisi** — le format seulement pour les ebooks, le type de document seulement s'il n'est pas `NonPrecise`, les rôles seulement à partir de deux auteurs. 11. **Deux `NULL` sont distincts pour SQLite.** Toute clé d'unicité personnelle (`AuteurNormalise`, `NumeroNormalise`) est `NOT NULL` avec un défaut vide. 12. **Le mode est dans l'URL**, jamais dans un booléen interne (`/livres/3` consulte, `/livres/3/edition` modifie) — et le composant s'abonne à `LocationChanged`, les deux routes partageant le même paramètre. 13. **`amd64.url` dans `manifest.toml` ne se change JAMAIS à la main**, et `depot_code` en tête de `publier.sh` porte la vraie URL du dépôt. La documentation, elle, est neutralisée en `forge.example.org` — anonymiser ce qui *sert* casserait le déploiement, et l'échec surviendrait bien plus loin, à l'installation. ### Deux pièges Blazor qui reviennent - **Une règle CSS scopée n'atteint pas ce que rend un composant enfant** (`NavLink` et tout composant du projet). Soit `::deep`, soit la feuille globale — et les règles du menu restent globales, la navigation étant ce qui doit le moins pouvoir tomber. - **Un composant qui écrit dans un objet prêté par son parent doit le prévenir** (`EventCallback`), sinon tout ce que le parent calcule à partir de cet objet reste figé sur l'état d'avant. ### Deux pièges de saisie - **Un champ relié sur `oninput` à une propriété qui découpe puis recompose est intapable** : son propre séparateur disparaît sous les doigts. `SaisieListe` porte la saisie un-par-un pour les auteurs, les thèmes et les articles à la une. - **`aria-expanded="@Booleen"` ne marche pas** : Blazor le traite comme un attribut de présence. Passer par `Aria.Etat(...)`. --- ## Travailler dans ce dépôt ```bash dotnet build && dotnet test ``` ```bash dotnet run --project MaBibli.Api ``` L'API sert aussi le client compilé : une seule commande suffit. - **Une seule migration**, `InitialCreate`, appliquée seule au premier démarrage. - **`dotnet ef` n'a pas besoin de démarrer l'application** (`FabriqueDbContextConception`). Sa chaîne de connexion ne sert qu'à la génération de code. - **Publier** : `mabibli_ynh/build/publier.sh`, seul script, seule source des trois valeurs qui doivent rester cohérentes (version, URL, sha256). `depot_code` en tête du script commande toutes les URL dérivées. Marche à suivre complète dans [`docs/publier-une-version.md`](docs/publier-une-version.md). - **Toute la documentation vit dans ce dépôt**. Ne pas recréer de guide dans `mabibli_ynh` — deux jeux de documentation pour une seule chaîne finissaient par diverger. Seules exceptions : `mabibli_ynh/doc/DESCRIPTION.md` et `mabibli_ynh/doc/ADMIN.md`, que **YunoHost lit lui-même**. - **Les écrans se vérifient sur la géométrie réelle** (`getBoundingClientRect`, `scrollWidth`) et les styles calculés, aux trois largeurs **320, 375 et 1280 px** — pas à l'œil. Un débordement de 8 px ne se voit sur aucune capture d'écran. - **La base de développement doit rester garnie** (un cycle, des tomes, une revue à plusieurs numéros, des envies), sans quoi la moitié des écrans s'affiche vide et ne prouve rien. ### Comment ce projet tranche - **Mesurer avant de tailler**, et **toujours comparer deux publish** : le coût d'une bibliothèque n'est pas le poids de son assembly, c'est la traîne qu'elle impose au trimmer. - **La vitesse ne dit rien du taux de réussite.** Un décodeur a été blanchi sur des mesures de vitesse alors qu'il *ratait* des codes. - **Une source intermittente produit exactement les symptômes d'une source lacunaire.** - **Ce qui ne prétend rien vaut mieux que ce qui prétend faux** : `NonPrecise` plutôt que `Roman`, `NULL` plutôt que `0` page, une tranche d'ISBN en moins plutôt qu'une fausse. --- ## Ce qui reste ouvert Rien ici n'est acté. ### Après la V1, si l'usage le demande - **Réordonner les envies de revues.** Elles ont un `Rang` mais aucun écran pour les déplacer ; la section est courte par nature. - **Filtrer le catalogue par thème.** Demanderait une dimension de plus dans les **deux** implémentations de filtrage, le contrat de cache et le test de confrontation. - **OpenLibrary en second rideau de la bibliographie par auteur.** ⚠️ À ne pas confondre avec la cascade ISBN, où OpenLibrary sert **déjà** de seconde source : la bibliographie, elle, n'interroge que le SRU de la BnF, donc rien des auteurs étrangers non traduits. Deux difficultés : résoudre un nom vers un identifiant OpenLibrary (`/search/authors.json`), sensible aux homonymes ; et des œuvres remontées **en langue originale**, donc non rapprochables du catalogue par `CleOeuvre`. - **Wikidata en troisième source ISBN**, seulement si la cascade BnF → OpenLibrary montre ses limites en usage. ### Écarté, et à ne pas rouvrir sans nouvelle demande | Item | Raison | |---|---| | **Photographier une couverture** | premier stockage de fichiers du projet, ce qu'« ebooks : fiches uniquement » écarte. Une URL se colle à la main | | **Traducteur, préfacier, photographe** dans les rôles | une valeur s'ajoute sans migration ; la renommer une fois posée sur des centaines de liens, non | | **Thèmes déduits d'un lookup** | le vocabulaire Rameau de la BnF est une indexation professionnelle, pas les étiquettes attendues | | **Réécrire le reboot Bootstrap** dans `app.css` | il porte les variables `--bs-*` dont ses propres règles dépendent, et un comportement vérifié sur 22 routes, pour 3 Ko |