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>
203 lines
12 KiB
Markdown
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 |
|