From a5392cedac24c21acd107b005aecd12449eb1858 Mon Sep 17 00:00:00 2001 From: mathieu Date: Mon, 17 Aug 2026 21:21:38 +0200 Subject: [PATCH] Acter les cinq decisions structurantes (offline, portee, ISBN, SSO, ebooks) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- CLAUDE.md | 68 ++++++++++++++++++++++++++++++++++++++++++------------- README.md | 5 ++-- 2 files changed, 55 insertions(+), 18 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 64582df..6456239 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,7 +27,9 @@ Application self-hosted de gestion de bibliothèque personnelle, à héberger su | 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 | Déléguée au SSO de YunoHost | Pas de système de login custom à maintenir | +| 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 | @@ -92,29 +94,57 @@ public static class IsbnScanner } ``` -## Stratégie hors-ligne — à trancher avant de coder +## 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 à OpenLibrary. +**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. -⚠️ **Point d'architecture non résolu** : « 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. Le service worker de la PWA met en cache les *assets* (HTML/CSS/WASM), mais **pas les réponses de l'API**. Sans travail supplémentaire, l'app se lancera hors-ligne mais affichera une liste vide. +⚠️ **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**. -Options à évaluer : +**Décision** : cache client des réponses `GET` de l'API (IndexedDB, ou cache du service worker), en **lecture seule**. -1. **Cache client des réponses API** (IndexedDB, ou cache du service worker sur les routes `GET`) — le plus simple, lecture seule, suffisant pour le besoin exprimé. -2. **SQLite compilé en WASM côté client**, avec persistance IndexedDB/OPFS, synchronisé avec le serveur — bien plus lourd, ne se justifie que si l'écriture hors-ligne devient nécessaire. - -Par défaut, partir sur l'option 1 tant que le besoin reste la consultation. +- 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) : +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). -1. **OpenLibrary** (`https://openlibrary.org/isbn/{isbn}.json`) — source principale, gratuite, sans clé +**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) -2. **Fallback envisageable** : autres sources ouvertes (à évaluer — éviter Google Books sauf si l'utilisateur change d'avis explicitement) +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) @@ -129,7 +159,7 @@ Livre ├── Statut : ALire | EnCours | Lu ├── CoverUrl ├── DateAjout -└── UtilisateurId (propriétaire, via SSO YunoHost) +└── AjoutePar (YNH_USER — traçabilité, PAS un cloisonnement) Pret ├── Id @@ -141,6 +171,10 @@ Pret 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 : @@ -164,6 +198,8 @@ Conclusion : aucune des deux solutions existantes ne coche toutes les cases (pr ## Questions ouvertes -- **Mécanisme exact du SSO YunoHost** : le principe est acté (pas d'auth custom), mais l'implémentation reste à préciser — vraisemblablement la lecture d'en-têtes injectés par SSOwat derrière nginx (`Remote-User`). Cela conditionne le remplissage de `UtilisateurId`. -- **Sources ISBN de fallback** : toujours « à évaluer ». Candidats sans clé API et hors Google : OpenLibrary Search API, BnF (SRU — pertinent pour le fonds francophone), Wikidata. -- **Choix du cache hors-ligne** : voir la section dédiée. +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. diff --git a/README.md b/README.md index 29b9313..9e25372 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,11 @@ Gérer une bibliothèque personnelle (livres physiques et numériques), avec : - **Frontend** : Blazor WebAssembly, en **PWA** (installable, utilisable hors-ligne pour la consultation de la bibliothèque déjà enregistrée) - **Scan code-barres** : **ZXing.Net** (décodage EAN-13 en C#, pas de bibliothèque JS tierce) - **Accès** : smartphone (GSM) et PC, via navigateur -- **Multi-utilisateur** : géré via le SSO de **YunoHost** (pas de système d'auth custom) +- **Multi-utilisateur** : géré via le SSO de **YunoHost** (en-têtes SSOwat, pas d'auth custom) — **collection commune** au foyer, avec traçabilité de qui a ajouté chaque livre - **Base de données** : **SQLite** - **Hébergement** : **YunoHost**, en installation **native (sans Docker)** — packaging façon `_ynh`, inspiré de [radarr_ynh](https://github.com/YunoHost-Apps/radarr_ynh) (déploiement .NET self-contained, pas de dépendance dotnet-runtime côté système) -- **Sources ISBN** : interrogation de **plusieurs bases de données libres/sans restriction** en cascade (pas de dépendance à une seule source propriétaire type Google Books) — voir `CLAUDE.md` pour le détail +- **Sources ISBN** : cascade **BnF (SRU) puis OpenLibrary** — bases libres, sans clé API, pas de dépendance à Google Books. La BnF passe en premier pour la couverture du fonds francophone. Voir `CLAUDE.md` pour le détail +- **Ebooks** : fiches d'inventaire uniquement, les fichiers ne sont pas hébergés par l'application ## Statut