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>
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
# 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 |
|
||||
Reference in New Issue
Block a user