Files
mabibli/CLAUDE.md
T
Mathieu LimonierandClaude Opus 5 6a6d745af4 MaBibli 1.0.0
Gestion de bibliothèque personnelle auto-hébergée : catalogue, prêts,
scan de code-barres, consultation hors-ligne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 22:36:16 +02:00

203 lines
12 KiB
Markdown

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