From b6c4dbaa081bfffb7d8f49281328a07942a889b7 Mon Sep 17 00:00:00 2001 From: mathieu Date: Sat, 22 Aug 2026 13:30:42 +0200 Subject: [PATCH] Decoupe la documentation par audience, et corrige trois defauts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le README faisait 691 lignes, dont 295 de procedure de mise en production — que personne ne lit avant d'avoir decide d'installer le projet. Or c'est le premier document lu. Il tombe a 86 lignes et devient un aiguillage. Un document, un lecteur, une question : README.md c'est quoi ? un visiteur docs/installer.md comment je l'heberge ? qui installe docs/publier-une-version.md comment je sors une v. ? qui maintient docs/architecture.md pourquoi le code ainsi ? qui contribue Trois defauts sortis de la comparaison entre le README et le tour du projet, dont deux introduits ce matin : ⚠ Le README se contredisait sur example.org. Son « Journal du paquet » documentait comme une panne reglee (« l'installation s'arretait net ») le placeholder que le nettoyage vient de retablir. Ce n'est pas une regression — publier.sh refuse desormais de publier avec, ce qui etait precisement le garde-fou manquant en aout — mais qui lisait le journal concluait l'inverse. Le journal est supprime : c'est de l'historique, il vit dans les commits et dans CLAUDE.md. ⚠ La fusion des migrations n'etait nulle part. La section « Monter de version » decrivait pas a pas une procedure qui echouerait depuis toute version anterieure a la 0.5.0, et son « Si la mise a jour echoue » ne mentionnait pas cette cause. L'avertissement est desormais en tete du README, d'installer.md et de la section concernee. ⚠ Les trois portees — commune, personnelle, trace — decident de tout dans ce projet et etaient absentes de son point d'entree. Elles ouvrent architecture.md. Verifie : aucune ancre morte, aucun lien mort, et les 41 lignes non reprises sont soit condensees dans le nouveau README, soit du journal supprime a dessein. Trois d'entre elles sont revenues (build/dist ignore, l'inspiration radarr_ynh, le renvoi aux commentaires de systemd.service). 626 tests au vert. Co-Authored-By: Claude Opus 5 --- README.md | 733 ++++-------------------------------- docs/architecture.md | 311 +++++++++++++++ docs/installer.md | 369 ++++++++++++++++++ docs/publier-une-version.md | 319 ++++++++++++++++ 4 files changed, 1063 insertions(+), 669 deletions(-) create mode 100644 docs/architecture.md create mode 100644 docs/installer.md create mode 100644 docs/publier-une-version.md diff --git a/README.md b/README.md index 79c6017..f270dcd 100644 --- a/README.md +++ b/README.md @@ -1,691 +1,86 @@ # MaBibli -Application de gestion de bibliothèque personnelle, self-hosted sur YunoHost. +Gestion de bibliothèque personnelle, auto-hébergée, pensée pour un foyer. +Catalogue, prêts, scan de code-barres, consultation hors-ligne. -Ce fichier est le **point d'entrée unique** du projet : ce qu'il fait, comment le -compiler, comment le publier, et comment l'installer sur un serveur. Deux fichiers -restent à part, chacun pour une bonne raison — `CLAUDE.md`, le contexte et l'historique -des décisions à destination de Claude Code, et `IDEES.md`, les pistes **non actées**. +- **Catalogue** des livres physiques et des ebooks — fiches uniquement, aucun fichier + n'est hébergé. Revues et séries ont leurs propres fiches. +- **Prêts** — à qui, depuis quand, et l'historique complet. +- **Scan ISBN** au code-barres depuis le téléphone, ou saisie manuelle, avec + pré-remplissage du titre, de l'auteur, de l'éditeur et de la couverture. +- **Statuts de lecture personnels** — chacun sa progression, sur une collection commune. +- **Liste d'envies** personnelle, exportable en `.txt` et `.csv`. +- **Consultation hors-ligne** — PWA installable ; la bibliothèque reste consultable et + cherchable sans réseau. + +Les métadonnées viennent de la **BnF** puis d'**OpenLibrary**, deux sources libres et +sans clé d'API. **Aucune dépendance à Google Books.** + +## Documentation + +| Document | Répond à | Pour qui | +|---|---|---| +| **[docs/installer.md](docs/installer.md)** | Comment je l'héberge ? | qui installe, sur YunoHost ou ailleurs | +| **[docs/publier-une-version.md](docs/publier-une-version.md)** | Comment je sors une version ? | qui maintient le projet | +| **[docs/architecture.md](docs/architecture.md)** | Pourquoi le code est ainsi ? | qui veut comprendre ou contribuer | +| `mabibli_ynh/doc/ADMIN.md` | Où sont les données, qui a accès ? | l'administrateur, après installation | + +`CLAUDE.md` porte le contexte complet et l'historique des décisions, avec leurs mesures — +y compris les raisonnements qui se sont révélés faux, gardés exprès pour ne pas les +reconduire. `IDEES.md` recueille les pistes **non actées**. + +## En bref + +Trois projets .NET, **un seul processus** en production : le client Blazor WebAssembly +est compilé en fichiers statiques que l'API sert elle-même. + +``` +MaBibli.Client ─┐ +MaBibli.Shared ─┼──► dotnet publish MaBibli.Api ──► un service systemd +MaBibli.Api ─┘ (self-contained) sur 127.0.0.1 +``` | | | |---|---| -| [Demande initiale](#demande-initiale) · [Contraintes techniques](#contraintes-techniques) | ce que l'application doit faire, et avec quoi | -| [Compilation](#compilation) | développer et produire l'artefact | -| [Le paquet YunoHost](#le-paquet-yunohost) | contenu du dépôt `mabibli_ynh`, points de conception | -| [Publication](#publication) | sortir une version, en une commande | -| [Mettre en production](#mettre-en-production) | installer, mettre à jour, revenir en arrière, désinstaller | +| Backend | C# / ASP.NET Core, **.NET 10** | +| Frontend | Blazor WebAssembly, en PWA | +| Base | SQLite + EF Core | +| Scan | **zbar** compilé en WebAssembly (LGPL-2.1) | +| Authentification | SSO YunoHost, via les en-têtes SSOwat — **pas d'auth propre** | +| Hébergement | YunoHost, installation **native** (sans Docker) | -## ⚠️ À faire avant de publier ce projet quelque part +Trois contraintes ne se négocient pas : **x86_64**, **HTTPS** (sans quoi le scan caméra +ne s'ouvre jamais) et un **domaine entier**, pas un sous-chemin. Le pourquoi est dans +[docs/architecture.md](docs/architecture.md). -Les URL de dépôt sont un **placeholder volontaire**, `https://forge.example.org/mabibli` -(le domaine `example.org` est réservé à la documentation par la RFC 2606, donc -définitivement injoignable). Rien ne fonctionnera tant qu'il n'est pas remplacé. - -**Une seule ligne commande tout le reste** — `depot_code`, en tête de -`mabibli_ynh/build/publier.sh`. Elle produit les URL de release, celles d'archive, et le -message de fin. Pour un essai ponctuel, `--depot-code https://…` fait la même chose sans -modifier le fichier. - -⚠️ **`publier.sh` refuse de publier tant que le placeholder est en place**, et il refuse -*avant* de compiler. C'est délibéré : publier avec une URL morte écrirait dans -`manifest.toml` une `amd64.url` que personne ne peut télécharger, et l'échec surviendrait -bien plus tard, à l'installation, sous la forme d'un `ynh_setup_source` en erreur — sans -rapport apparent avec la cause. - -Restent à changer à la main, une fois seulement : - -| Fichier | Quoi | -|---|---| -| `mabibli_ynh/manifest.toml` | `code = "…"`, et `maintainers` | -| `mabibli_ynh/conf/systemd.service` | `Documentation=…` | -| `mabibli_ynh/doc/ADMIN.md` | le lien de bas de page | -| `mabibli_ynh/README.md`, ce fichier | les liens et les commandes d'exemple | -| `MaBibli.Client/Pages/APropos.razor` | `Depot`, `Site`, et l'adresse de contact | - -⚠️ **`amd64.url` dans le manifeste n'est PAS à changer à la main** : `publier.sh` la -réécrit à chaque publication. La corriger donnerait l'illusion que c'est fait alors que la -source du problème est ailleurs. - -⚠️ **Le dépôt du code doit être PUBLIC.** `ynh_setup_source` télécharge sans jeton, et -Gitea comme GitHub répondent **404** — pas 403 — à un anonyme sur un dépôt privé. Le -symptôme est alors rigoureusement identique à « la release n'existe pas ». Contrôle qui -tranche, hors session authentifiée : `curl -fsSLI ""`. - -## Demande initiale - -Gérer une bibliothèque personnelle (livres physiques et numériques), avec : - -- **Liste des livres physiques** -- **Liste des livres ebooks** -- **Gestion de prêts** — prêter un livre à quelqu'un, marquer comme récupéré, historique des prêts -- **Récupération automatique des infos via ISBN** (titre, auteur, éditeur, couverture) — scan caméra + saisie manuelle -- **Consultation hors-ligne** de la bibliothèque existante (le lookup ISBN, lui, nécessite le réseau) -- **Statuts de lecture** — à lire, en cours, lu, etc. - -## Contraintes techniques - -- **Langage** : C# / ASP.NET Core -- **Frontend** : Blazor WebAssembly, en **PWA** (installable, utilisable hors-ligne pour la consultation de la bibliothèque déjà enregistrée) -- **Scan code-barres** : **zbar** (WebAssembly, LGPL-2.1) — voir `CLAUDE.md` pour l'historique de la bascule depuis ZXing.Net -- **Accès** : smartphone (GSM) et PC, via navigateur -- **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** : 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 - -## Compilation - -Prérequis : le SDK **.NET 10** (`dotnet --version`). - -### En développement +## Démarrer ```bash -dotnet build -dotnet test +dotnet build && dotnet test ``` -Pour lancer l'application en local (l'API sert aussi le client Blazor compilé) : - ```bash dotnet run --project MaBibli.Api ``` -### Publication self-contained (ce que le serveur reçoit réellement) +L'API sert aussi le client compilé : une seule commande suffit. -Le serveur YunoHost ne compile jamais : il télécharge une archive déjà publiée et la -déploie telle quelle (voir « Publication » ci-dessous). Pour produire ce même artefact -en local, à des fins de vérification : +## ⚠️ Avant de publier ce projet quelque part -```bash -dotnet publish MaBibli.Api --configuration Release --runtime linux-x64 --self-contained -``` +Les URL de dépôt sont un **placeholder volontaire**, `https://forge.example.org/mabibli`. +Une seule ligne les commande toutes — `depot_code`, en tête de +`mabibli_ynh/build/publier.sh` — et `publier.sh` **refuse de publier tant qu'elle n'est +pas changée**. Le détail est dans +[docs/publier-une-version.md](docs/publier-une-version.md). -`MaBibli.Api` référence `MaBibli.Client` : le client Blazor WebAssembly est compilé au -passage et atterrit dans `wwwroot/` du publish. Un seul projet à publier suffit donc -pour les trois (`MaBibli.Client`, `MaBibli.Shared`, `MaBibli.Api`). `--self-contained` -embarque le runtime .NET dans le dossier produit : aucun `dotnet-runtime` n'est requis -côté serveur. +## ⚠️ Depuis la 0.5.0, aucune base antérieure n'est migrable -Pour un hébergement **sans YunoHost** (Synology, VPS), voir « Installer ailleurs que sur -YunoHost » plus bas : le `Dockerfile` de la racine fait ce même publish, mais sans -`--self-contained` — dans un conteneur, l'image `aspnet` fournit déjà le runtime. +Les migrations ont été fondues en une seule. Sur un serveur déjà installé, il faut +`remove --purge` puis réinstaller, **avec perte du catalogue** — voir +[docs/installer.md](docs/installer.md). -⚠️ **Ne jamais compiler sur le serveur YunoHost lui-même** : ce serait imposer le SDK -complet à une machine qui n'en a pas besoin, pour une compilation lente. Voir -`CLAUDE.md`, section « Chaîne de publication ». +## Licence -## Le paquet YunoHost - -MaBibli se déploie via un **second dépôt**, [`mabibli_ynh`](https://forge.example.org/mabibli_ynh), -qui **ne contient aucun code C#** : il porte le manifeste, les configurations nginx et -systemd, et les scripts d'installation. Toute la documentation, elle, vit ici — ce -fichier est le point d'entrée unique du projet. - -### Contenu - -| Fichier | Rôle | -|---|---| -| `manifest.toml` | Identité, version, URL de l'archive et son `sha256`, ressources (utilisateur système, répertoires, port, permissions) | -| `conf/systemd.service` | Unité du service — écoute sur `127.0.0.1`, base dans le répertoire de données, durcissement | -| `conf/nginx.conf` | Reverse proxy et intégration SSOwat | -| `scripts/_common.sh` | Variables partagées et sauvegarde/restauration cohérente de la base SQLite | -| `scripts/install` `remove` `upgrade` `backup` `restore` | Cycle de vie de l'application | -| `build/publier.sh` | **Seul script de publication** : version, archive, manifeste, commit et push du paquet, tag et push du code. `--archive-seule` compile et archive sans rien publier, pour reprendre après un échec | -| `doc/DESCRIPTION.md` `doc/ADMIN.md` | Textes affichés par YunoHost lui-même (catalogue et interface d'administration) — ils doivent rester dans ce dépôt | - -### Points de conception - -**Le service n'écoute que sur `127.0.0.1`.** L'application déduit l'identité de l'en-tête -`YNH_USER` injecté par SSOwat ; cet en-tête n'est digne de confiance que si nginx est le seul -chemin d'accès. Un service exposé sur le réseau permettrait de forger `YNH_USER` et de -contourner le portail. La contrainte est écrite dans `mabibli_ynh/conf/systemd.service` (`ASPNETCORE_URLS`) -et le port n'est pas ouvert au pare-feu. - -**Aucune compilation sur le serveur.** Le publish est *self-contained* : il embarque son propre -runtime .NET, donc aucun paquet `dotnet-runtime` n'est nécessaire. `ynh_setup_source` vérifie le -`sha256` de l'archive avant de la déployer. - -**Les données survivent aux mises à jour.** La base SQLite vit dans le répertoire de données, pas -à côté du binaire ; `upgrade` ne remplace que le répertoire d'installation. - -**La sauvegarde passe par l'API de sauvegarde en ligne de SQLite.** La base est en mode WAL : -copier le seul fichier `.db` d'une base active peut ne rien sauvegarder du tout. Voir -`mabibli_ynh/doc/ADMIN.md`. - -## Publication - -Le geste courant tient en une commande, `build/publier.sh` du dépôt du paquet : - -```bash -cd ../mabibli_ynh -./build/publier.sh -``` - -Il reprend la version déjà publiée (tag le plus élevé, recoupé avec `manifest.toml`), -propose l'incrément, puis enchaîne tout ce qui suit : archive, manifeste, commit et push -du paquet, tag et push du code, avec vérification côté distant. Il ne reste qu'à créer -la release dans Gitea et y téléverser l'archive — le script en rappelle l'URL. - -⚠️ **Il n'y a qu'un seul script, et c'est délibéré.** Un second, `publier-release.sh`, -a existé jusqu'au 2026-08-21 : il compilait, archivait et réécrivait le manifeste, à -charge pour l'appelant de committer. Son nom inspirait plus confiance que celui du vrai -point d'entrée, et lancé seul il produisait exactement la panne du 2026-08-21 — un -`manifest.toml` corrigé mais non poussé, et une release `v0.4.0` contenant en réalité six -commits de plus que son tag. Les deux scripts sont fondus ; ce que le second savait -faire subsiste sous la même commande : - -```bash -./build/publier.sh --archive-seule -``` - -Ce mode compile et archive, **sans toucher ni au manifeste ni à git** : il ne peut donc -rien laisser à moitié publié. - -⚠️ **À quoi il sert vraiment** : à **reprendre une publication à la main** quand -`publier.sh` a échoué en cours de route (voir B.3). Ce n'est pas « une brique pour une -CI » — c'était la justification écrite jusqu'au 2026-08-22, et elle désignait un usage -qui n'a jamais eu lieu, là où la reprise après échec, elle, est documentée pas à pas. - -⚠️ **Les contrôles ne sont PAS désactivés dans ce mode**, contrairement à ce qui a été -vrai jusqu'au 2026-08-22 : ils s'exécutent tous, et **avertissent** au lieu de refuser, -puisque rien n'est publié et que rien ne peut donc mentir. Le dépôt sale, en -particulier, n'était alors pas signalé du tout — l'archive contenait en silence du -travail non committé. - -Ce script compile ce dépôt (`mabibli`) en `Release` self-contained, **vérifie** le -résultat (binaire présent, `wwwroot/` embarqué, toutes les ressources d'`index.html` -réellement sur disque, aucun placeholder d'empreinte non substitué), produit une -archive `.tar.gz` reproductible, calcule son `sha256`, et met à jour `version`, -`amd64.url` et `amd64.sha256` dans `mabibli_ynh/manifest.toml` — sauf en -`--archive-seule`. - -Il injecte au passage, dans l'assembly, la **version** (`-p:Version`) et un -**horodatage de build** (`-p:MaBibliDateBuild`), que la page « À propos » de -l'application affiche via `GET /api/version`. - -⚠️ **L'horodatage est le témoin de l'injection** : sans lui, l'application se déclare -« version de développement » plutôt que d'afficher le `1.0.0` que le SDK .NET pose par -défaut. Ce numéro-là se lirait comme une vraie version alors qu'il ne désigne rien — or -c'est précisément la valeur qu'on va chercher pour diagnostiquer un appareil dont le -cache est dépareillé. Un `dotnet build` local n'annonce donc aucune version, et c'est -voulu. - -⚠️ YunoHost lit le manifeste **depuis Gitea**, jamais une copie locale : un manifeste -corrigé mais non poussé n'existe pas pour le serveur. C'est précisément ce que -`publier.sh` empêche d'oublier. - -Il refuse par ailleurs de publier dans deux cas, **avant de compiler** : - -| Refus | Ce qu'il évite | -|---|---| -| le tag `vX.Y.Z` existe déjà | une release qui ne contient pas le code que son tag désigne — arrivé le 2026-08-21, six commits d'écart, sans que rien ne le signale | -| le dépôt du code a des modifications non committées | une archive contenant du travail que le tag, lui, ne contient pas | - -Dans les deux cas il nomme le commit en cause et rappelle `--patch`. ⚠️ En -`--archive-seule`, les mêmes contrôles **avertissent** au lieu de refuser — le constat -s'affiche, le conseil « choisissez un numéro libre » non : en reprise, le tag visé -est justement celui qu'on veut retrouver. - -**La marche à suivre complète et éprouvée**, pour les cas que `publier.sh` ne couvre -pas — première mise en production, montée de version pas à pas, retour arrière, -désinstallation — est en « Mettre en production » ci-dessous. - -## Mettre en production - -Deux marches à suivre, éprouvées sur un serveur réel le 2026-08-18 : la **première mise -en production**, puis la **montée de version**. `publier.sh` (ci-dessus) enchaîne les -étapes courantes ; ce qui suit les détaille une à une, pour les comprendre et pour les -cas qu'il ne couvre pas. - -``` -machine de dev Gitea serveur YunoHost -────────────── ───── ──────────────── -publier.sh - → archive .tar.gz ────────► release du dépôt `mabibli` - → manifest.toml ────────► dépôt `mabibli_ynh` ──────► yunohost app install - (url + sha256) (vérifie le sha256) -``` - -### A. Première mise en production - -#### A.1 Préalables, une seule fois - -- Le dépôt du code (`mabibli`) est **public** sur Gitea. `ynh_setup_source` télécharge - sans jeton ; sur un dépôt privé Gitea répond 404 — indiscernable d'une release - absente. -- Le domaine dédié existe côté YunoHost. MaBibli s'installe sur un **domaine entier** - (`mabibli.mondomaine.tld`), jamais sur un sous-chemin : - -```bash -sudo yunohost domain add mabibli.mondomaine.tld -``` - -- Le SDK .NET est installé sur la machine de développement (`dotnet --version`). - -#### A.2 Produire l'archive, le manifeste et le tag - -Depuis `mabibli_ynh`, avec le dépôt du code à côté (`../mabibli`) : - -```bash -./build/publier.sh --version 0.1.0 -``` - -Le script compile en `Release` self-contained, **vérifie le publish** (binaire présent, -`wwwroot/` embarqué, toutes les ressources d'`index.html` réellement sur disque, aucun -placeholder d'empreinte non substitué), produit l'archive de façon reproductible, en -calcule le `sha256`, réécrit `version`, `amd64.url` et `amd64.sha256` dans -`manifest.toml`, **commite et pousse le paquet**, puis **tague et pousse le code**. - -À contrôler dans sa sortie : la ligne `binaire, wwwroot et ressources d'index.html : OK`, -le `sha256` affiché — c'est celui que le manifeste porte désormais — et les deux `✓` de -la vérification côté distant. - -Il ne reste alors que A.3. Les étapes A.4 et suivantes décrivent ce que le script vient -de faire, pour le cas où l'on reprend à la main après un échec en cours de route -(`--archive-seule` s'arrête après l'archive, sans toucher au manifeste ni à git). - -#### A.3 Déposer la release - -Le tag `v0.1.0` est déjà poussé par A.2 (`git tag v0.1.0 && git push origin main --tags` -si l'on reprend à la main). - -**Dans l'interface Gitea** : créer la release `v0.1.0` sur le dépôt `mabibli`, et y -téléverser `build/dist/mabibli-0.1.0-linux-x64.tar.gz`. - -Vérification, à faire **sans être authentifié** (autre navigateur, ou `curl` comme -ci-dessous) : - -```bash -curl -fsSLI "https://forge.example.org/mabibli/releases/download/v0.1.0/mabibli-0.1.0-linux-x64.tar.gz" | head -1 -``` - -Attendu : `HTTP/2 200`. Un 404 signifie soit que la release n'est pas déposée, soit que -le dépôt est privé — les deux se ressemblent, commencer par vérifier la visibilité. - -#### A.4 Le paquet, sur Gitea - -⚠️ **L'étape la plus facile à oublier quand elle est manuelle** — et celle qui a fait -échouer la mise à jour du 2026-08-21. `yunohost app install ` lit le manifeste -**depuis Gitea**, jamais la copie locale : un manifeste corrigé mais non poussé n'existe -pas pour le serveur. A.2 s'en charge et vérifie l'arrivée par `git ls-remote` ; en -reprise à la main : - -```bash -cd ../mabibli_ynh && git add -A && git commit -m "Publier la version 0.1.0" && git push -``` - -Le contrôle qui tranche, quel que soit le chemin suivi — c'est le manifeste **tel que le -serveur le lira** : - -```bash -curl -fsSL "https://forge.example.org/mabibli_ynh/raw/branch/main/manifest.toml" | grep amd64 -``` - -#### A.5 Installer - -```bash -sudo yunohost app install https://forge.example.org/mabibli_ynh --debug -``` - -YunoHost demande le domaine (celui créé en A.1) et le groupe autorisé (`all_users`). - -**Ce qu'il faut voir passer :** `Prefetching asset main` sur la bonne URL, puis -l'installation des fichiers, nginx, systemd, et enfin `The service mabibli has correctly -executed the action start` — le script attend la ligne `Application started` du journal, -ce qui fait échouer franchement l'installation si les migrations EF Core ne passent pas. - -#### A.6 Vérifier - -```bash -sudo ss -tlnp | grep -i mabibli -``` - -Doit montrer **`127.0.0.1:` uniquement**. C'est une frontière de sécurité : sur -`0.0.0.0`, n'importe qui sur le réseau pourrait forger l'en-tête `YNH_USER` et se faire -passer pour un membre du foyer. - -```bash -sudo ls -l /home/yunohost.app/mabibli -``` - -`mabibli.db` doit exister : les migrations se sont appliquées seules au premier -démarrage. (Pas de `-wal` ni `-shm` au repos, c'est normal — SQLite fait un checkpoint à -la fermeture de la dernière connexion.) - -Enfin, dans un navigateur sur `https://mabibli.mondomaine.tld` : le portail authentifie, -et l'application affiche **ton** nom d'utilisateur YunoHost — pas « anonyme ». C'est le -seul contrôle qui éprouve réellement l'intégration SSO. - ---- - -### B. Monter de version — exemple : 0.1.0 → 0.1.1 - -#### B.1 Ce qui distingue une version applicative d'une révision de paquet - -| Ce qui change | Version | Nouvelle archive ? | -|---|---|---| -| Le code C# | `0.1.0` → `0.1.1~ynh1` | **oui** | -| Seulement le paquet (conf systemd/nginx, scripts) | `0.1.0~ynh1` → `0.1.0~ynh2` | non | - -Le second cas est le plus simple : bump du suffixe `~ynhN` **à la main** dans -`manifest.toml`, commit, push, puis directement l'étape B.5. L'archive et son `sha256` -ne bougent pas. (C'est ce qui a été fait pour `~ynh2` et `~ynh3`.) - -La suite décrit le premier cas. - -#### B.2 En une commande : `publier.sh` - -C'est le geste courant, décrit en « [Publication](#publication) » plus haut : il enchaîne -B.3, B.4 et B.5 (archive, manifeste, commit et push du paquet, tag et push du code), et -vérifie sur le distant que tout est arrivé. - -```bash -cd ../mabibli_ynh && ./build/publier.sh -``` - -La suite décrit ces mêmes étapes **à la main**, pour comprendre ce que le script fait et -pour les cas qu'il ne couvre pas : révision `~ynhN` seule, reprise après un échec en -cours de route. - -#### B.3 Compiler et vérifier la nouvelle version - -Le code est prêt et committé dans `mabibli`. Depuis `mabibli_ynh` : - -```bash -./build/publier.sh --archive-seule --patch -``` - -⚠️ **`--version` n'est plus obligatoire ici, depuis le 2026-08-22.** Le mode reprenait -alors la version de `manifest.toml` — c'est-à-dire celle **déjà publiée** — et -reproduisait donc 0.1.0 tant qu'on ne le corrigeait pas à la main. Les deux modes -calculent maintenant la version de la même façon : `--patch` propose 0.1.1, et -`--version` ne sert plus qu'à viser un autre numéro (par exemple celui d'un tag déjà -créé, si la publication a échoué après le tag). - -⚠️ **`--archive-seule` ne touche pas au manifeste** : `version` (avec son suffixe -`~ynh1` — une nouvelle version applicative repart toujours de 1), `amd64.url` et -`amd64.sha256` sont alors à réécrire à la main. C'est le prix de la marche à pied ; -`publier.sh` sans option le fait, et ne peut pas l'oublier. - -Note le `sha256` affiché — il ne sera plus jamais le même, même à code identique si les -dépendances bougent. - -#### B.4 Tag et release - -```bash -cd ../mabibli && git tag v0.1.1 && git push origin main --tags -``` - -Créer la release `v0.1.1` dans Gitea, y téléverser -`build/dist/mabibli-0.1.1-linux-x64.tar.gz`, puis vérifier sans authentification : - -```bash -curl -fsSLI "https://forge.example.org/mabibli/releases/download/v0.1.1/mabibli-0.1.1-linux-x64.tar.gz" | head -1 -``` - -#### B.5 Pousser le paquet - -```bash -cd ../mabibli_ynh && git add -A && git commit -m "Publier la version 0.1.1" && git push -``` - -#### B.6 Sauvegarder, puis mettre à jour - -YunoHost prend **lui-même** une sauvegarde de sécurité avant la mise à jour -(`mabibli-pre-upgrade1`), mais elle **ne contient pas le répertoire de données** -(`BACKUP_CORE_ONLY`) : elle sert à restaurer l'application, pas la bibliothèque. Prendre -une sauvegarde complète reste donc utile avant une version qui touche au schéma : - -```bash -sudo yunohost backup create --apps mabibli -``` - -```bash -sudo yunohost app upgrade mabibli -u https://forge.example.org/mabibli_ynh --debug -``` - -⚠️ **L'option `-u` n'est pas facultative ici.** MaBibli n'est pas dans le catalogue -officiel : sans elle, YunoHost ne sait pas où retrouver le paquet et refuse d'emblée — -« mabibli is not in the catalog (anymore?) », puis « No apps can be upgraded ». Rien -n'est cassé pour autant, la commande n'a simplement pas commencé. C'est la **même URL** -qu'à l'installation, celle du dépôt `_ynh`, jamais celle de l'archive. - -(Ajouter `--force` seulement pour réappliquer une version identique, par exemple en -mise au point du paquet.) - -Le script arrête le service **avant** de remplacer les binaires — deux processus sur la -même base SQLite pendant une migration est exactement ce qu'il faut éviter — puis -attend `Application started`, avec un délai de 120 s : une migration sur base remplie -prend plus de temps que la création d'un schéma vide. - -#### B.7 Vérifier après mise à jour - -```bash -sudo systemctl status mabibli --no-pager && sudo journalctl -u mabibli -n 30 --no-pager -``` - -Puis, dans le navigateur, le contrôle qui compte vraiment : **les livres sont toujours -là**. C'est ce qui valide que la base vit bien dans le répertoire de données et non à -côté du binaire — `upgrade` remplace intégralement `/var/www/mabibli`. - -Vider le cache du navigateur n'est pas nécessaire : le service worker compare les -empreintes et propose « Mettre à jour ». Sur mobile, un onglet resté ouvert peut -retarder la bascule — c'est précisément ce que le bandeau de mise à jour sert à -débloquer. - -#### B.8 Si la mise à jour échoue - -YunoHost restaure automatiquement la sauvegarde de sécurité quand le script échoue. Si -le service démarre mais que l'application se comporte mal, revenir en arrière à la main : - -```bash -sudo yunohost backup list -``` - -```bash -sudo yunohost app remove mabibli --purge -``` - -```bash -sudo yunohost backup restore --apps mabibli -``` - -⚠️ `--purge` efface le répertoire de données. Ne le faire qu'avec une archive contenant -la bibliothèque sous la main — celle de B.5, pas `mabibli-pre-upgrade1`. - ---- - -### C. Désinstaller - -```bash -sudo yunohost app remove mabibli -``` - -Retire le service, la conf nginx, la permission SSO, l'utilisateur système, le port et -`/var/www/mabibli`. **`/home/yunohost.app/mabibli` survit**, donc la bibliothèque aussi : -une désinstallation faite trop vite ne doit pas être irréversible. - -⚠️ Corollaire à connaître en phase d'essai : réinstaller après un `remove` sans purge -**retrouve l'ancienne base**. Ce n'est pas une installation vierge, même si tout le -reste est neuf. - -Pour tout effacer, données comprises : - -```bash -sudo yunohost app remove mabibli --purge -``` - -Contrôle qu'il ne reste rien : - -```bash -systemctl status mabibli; sudo ls -d /var/www/mabibli /home/yunohost.app/mabibli 2>&1; getent passwd mabibli -``` - -Les trois doivent être négatifs. Le domaine, lui, reste déclaré dans YunoHost. - ---- - -## Installer ailleurs que sur YunoHost (Synology, NAS, VPS) - -Le déploiement de référence reste le paquet `mabibli_ynh` : c'est lui qui est éprouvé, et -c'est le seul qui apporte une **authentification**. Ce qui suit sert aux hébergements qui -n'ont pas de portail SSO. `Dockerfile`, `.dockerignore` et `compose.yaml`, à la racine de -ce dépôt, existent pour ça. - -L'application n'est qu'**un processus et un fichier SQLite** : `MaBibli.Api` sert lui-même -le client Blazor. Il n'y a donc rien à orchestrer. - -### ⚠️ Hors YunoHost, il n'y a AUCUNE authentification - -C'est le point à comprendre avant tout le reste, et il ne se contourne pas par la -configuration. `FournisseurUtilisateurSsowat` lit l'en-tête `YNH_USER` que le portail -injecte ; sans portail, il n'y a pas d'identité, et l'application **ne s'en invente pas**. - -Conséquence exacte, vérifiée en exécution : - -| | Sans identité | -|---|---| -| Catalogue, auteurs, séries, revues, prêts (**communs** au foyer) | fonctionnent | -| Statuts de lecture, listes d'envies (**personnels**) | refusés — `400 « Impossible d'ajouter une envie sans savoir à qui elle appartient. »` | - -Deux façons de rendre une identité, **aucune des deux n'authentifie qui que ce soit** : - -- `Identite__UtilisateurSimule` en variable d'environnement. C'est le mécanisme prévu pour - le développement, mais c'est de la configuration ordinaire : elle vaut aussi en - production. Mono-utilisateur. -- l'en-tête `YNH_USER` injecté par le proxy inversé (DSM : *Portail des applications > - Proxy inversé > En-tête personnalisé*). Même niveau de sécurité, mais si un proxy - authentifiant est ajouté un jour (Authelia, authentik), il n'y a plus qu'à lui faire - recopier son `Remote-User` vers `YNH_USER` : l'application n'a rien à changer. - -⚠️ Dans les deux cas, **quiconque atteint le port EST cet utilisateur**. Le port ne doit -donc être publié que sur la boucle locale (`127.0.0.1:8080:8080`), le proxy inversé restant -le seul chemin d'accès. C'est la transposition exacte de la contrainte d'écoute que le -paquet YunoHost pose comme dure. - -### Trois prérequis qui ne se négocient pas - -- **Une machine x86_64.** Le publish visé est `linux-x64` : les NAS ARM (les modèles « j » - notamment) demanderaient de republier en `linux-arm64`, ce qui n'est pas éprouvé ici. -- **HTTPS**, sans quoi le scan du code-barres ne s'ouvrira **jamais** : la caméra exige un - contexte sécurisé. Sur Synology, cela veut dire proxy inversé + certificat Let's Encrypt - sur un nom DDNS (`mabibli.xxx.synology.me`). -- **Un nom d'hôte entier, pas un sous-chemin** — `mabibli.xxx.synology.me`, jamais - `nas.xxx.synology.me/mabibli`. La raison est la même que pour le `full_domain` de - YunoHost, et elle est développée plus bas : `` et les empreintes du - service worker sont figés à la compilation. - -### La marche à suivre - -```bash -git clone mabibli && cd mabibli -# éditer compose.yaml : nom d'utilisateur, chemin du volume -docker compose up -d --build -``` - -Sur Synology, le même `compose.yaml` se colle dans *Container Manager > Projet*, en -remplaçant `./donnees` par un chemin réel (`/volume1/docker/mabibli`). - -Les migrations EF Core s'appliquent seules au premier démarrage : la base se crée dans le -volume, et c'est **le volume seul** qui la fait survivre au remplacement de l'image. - -### Ce qu'il faut savoir avant de s'y mettre - -- **La sauvegarde ne se fait pas en copiant le `.db`.** SQLite tourne en mode WAL : copier - le seul fichier d'une base active peut ne rien sauvegarder du tout. Il faut arrêter le - conteneur, ou passer par `sqlite3 … ".backup"` — c'est ce que fait le paquet YunoHost, et - la raison est écrite dans `mabibli_ynh/doc/ADMIN.md`. -- **Il n'y a pas de mécanisme de mise à jour.** Ni `yunohost app upgrade`, ni release, ni - vérification de `sha256` : à chaque version, `git pull` puis `docker compose up -d - --build`. Les données ne bougent pas, elles sont dans le volume. -- **L'image est construite depuis les sources, pas depuis une release.** Elle ne porte donc - aucun numéro de version, et « À propos » affiche honnêtement « version de développement ». - Pour un vrai numéro, passer `-p:Version=X.Y.Z -p:MaBibliDateBuild=…` au `dotnet publish` - du `Dockerfile` — voir `CLAUDE.md`, « Y — l'application dit sa version ». - -### Vérifié en exécution le 2026-08-21 - -Image construite et démarrée localement, base neuve dans un volume monté : - -| Cas | Résultat | -|---|---| -| Migrations au premier démarrage | appliquées, `mabibli.db` créé dans le volume | -| `GET /` et `_framework/blazor.webassembly.js` | **200**, `` intact | -| `GET /api/moi` avec `Identite__UtilisateurSimule` | `{"identifiant":"prenom","simule":true}` | -| `GET /api/moi` sans rien | `{"identifiant":null,"affichage":"inconnu"}` | -| `GET /api/moi` avec en-tête `YNH_USER: camille` | `{"identifiant":"camille","simule":false}` | -| `POST /api/souhaits` sans identité | **400**, message lisible | - -⚠️ **Ce qui n'a PAS été vérifié** : l'installation sur un Synology réel, le proxy inversé de -DSM et le certificat. Ce sont des gestes de DSM, pas du code — mais personne ne les a -éprouvés ici. - ---- - -## Contraintes permanentes du paquet — ce ne sont pas des tâches - -### Le dépôt du code doit rester public - -`ynh_setup_source` télécharge **sans jeton**. Sur un dépôt privé, Gitea répond -**404 et non 403** à un anonyme : le symptôme est identique à « la release n'existe -pas », ce qui envoie chercher au mauvais endroit. Ne pas mettre de jeton dans l'URL -du manifeste — il serait lisible sur le serveur. - -### L'application exige un domaine entier, pas un sous-chemin - -MaBibli s'installe sur `mabibli.mondomaine.tld`, **pas** sur `mondomaine.tld/mabibli`. - -Deux éléments sont figés **à la compilation** du client Blazor WebAssembly : la balise -`` de `index.html`, et les empreintes d'intégrité de -`service-worker-assets.js`. Les réécrire sur le serveur casserait le service worker, -donc le mode hors-ligne — et rien n'est recompilé sur le serveur, c'est tout l'intérêt -du self-contained. Le paquet déclare donc l'application en `full_domain`. - -Lever cette contrainte demanderait une archive **par chemin d'installation**, ou une -compilation sur le serveur. Les deux annulent le bénéfice du self-contained. - -### `install_dir` finit par appartenir à l'application, pas à root - -Le manifeste demande `owner = "root:rwx"` pour que le service ne puisse pas réécrire -ses binaires, mais le helper `_ynh_apply_default_permissions` repasse derrière avec un -`chown -R mabibli:mabibli`. L'intention tient quand même, portée par -`ProtectSystem=strict` dans l'unité systemd : tout est en lecture seule sauf -`ReadWritePaths=`, qui ne liste que le `data_dir`. - -⚠️ Ne pas retirer `ProtectSystem=strict` en croyant que la propriété des fichiers -protège encore. - ---- - -## Journal du paquet — ce qui a été réglé - -### Les deux pièges systemd — 2026-08-18 - -Découverts à la première installation réelle, chacun a coûté un cycle complet. Ni -l'un ni l'autre ne peut sortir d'un lancement du binaire à la main : ils tiennent au -gestionnaire de services. - -| Piège | Symptôme | Correction | -|---|---|---| -| `Environment=` découpe sur les espaces | `ArgumentException … at index 0` | guillemeter toute la ligne | -| `ProtectHome=yes` masque `/home` | `SQLite Error 14: unable to open database file` | `ProtectHome=tmpfs` + `BindPaths=` | - -Détail et mesures dans les commentaires de `mabibli_ynh/conf/systemd.service`, à ne pas retirer. - -### L'URL de démonstration a été remplacée — 2026-08-18 - -`manifest.toml` portait `gitea.example.org` : l'installation s'arrêtait net au -`Prefetching asset main`. Corrigé partout (`manifest.toml`, ce fichier, -`conf/systemd.service`, et la valeur par défaut de `--base-url`). Contrôle : -`grep -rn "gitea.example.org" .` ne doit plus rien remonter que ce fichier-ci. - -### L'archive ne va plus dans git — 2026-08-18 - -`build/dist/` est ignoré. L'archive vit dans la release Gitea ; committée, elle -ajouterait 69 Mo d'historique **par version**, définitivement. - -## Statut - -Voir `CLAUDE.md` pour le contexte complet et l'historique des décisions à destination -de Claude Code. +**AGPL v3** — voir [LICENSE](LICENSE). L'AGPL attend que les utilisateurs d'un service en +réseau puissent en obtenir la source : le lien vers le dépôt affiché dans la page +« À propos » de l'application n'est pas un ornement. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..a29ae44 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,311 @@ +# MaBibli, partie par partie + +Ce que fait chaque morceau, la décision qui le tient, et le piège à ne pas rouvrir. + +Pour installer, voir [installer.md](installer.md). Pour sortir une version, +[publier-une-version.md](publier-une-version.md). Le détail complet des décisions, avec +leurs mesures et les raisonnements qui se sont révélés faux, est dans `CLAUDE.md`. + +| | | +|---|---| +| [La forme](#la-forme--trois-projets-un-seul-processus) | trois projets, un seul processus | +| [Le modèle](#le-modèle--trois-portées-et-tout-en-découle) | trois portées, et tout en découle | +| [Le serveur](#le-serveur--enrichir-une-fiche-depuis-un-code-barres) | enrichir une fiche depuis un code-barres | +| [Le client](#le-client--consulter-dabord-modifier-ensuite) | consulter d'abord, modifier ensuite | +| [Le hors-ligne](#le-hors-ligne--consultation-seule-et-neuf-instantanés) | consultation seule, neuf instantanés | +| [Le déploiement](#le-déploiement--sso-par-en-têtes-et-une-frontière-dure) | SSO par en-têtes, et une frontière dure | +| [La méthode](#la-méthode-qui-est-peut-être-le-vrai-acquis) | comment le projet tranche | + +--- + +## La forme : trois projets, un seul processus + +Le client Blazor WebAssembly **n'est pas un serveur**. Compilé, ce ne sont que des +fichiers statiques que l'API sert depuis son `wwwroot`. Un seul processus tourne donc +en production. + +``` +MaBibli.Client ─┐ ┌─ 10 368 lignes +MaBibli.Shared ─┼──► dotnet publish ──► un ├─ 3 100 (entités, DTO, ISBN, normalisation) +MaBibli.Api ─┘ (self-contained) service└─ 9 314 + + systemd · 127.0.0.1:PORT · nginx · SSOwat +``` + +`MaBibli.Shared` n'est pas un fourre-tout : il porte ce que **les deux côtés doivent +calculer à l'identique** — la normalisation du texte, le rapprochement d'auteurs, le +formatage des ISBN. Un écart entre client et serveur y deviendrait un bug invisible. + +### ⚠️ Le piège du publish, résolu et à ne pas rouvrir + +Les **empreintes WASM sont désactivées** (`WasmFingerprintAssets=false`), et il ne faut +pas les réactiver. Publier l'API réécrivait mal `index.html` : les placeholders restaient +littéraux, la racine répondait 200 et l'application restait blanche. + +**Vérifier que `/` renvoie 200 ne prouve rien** — c'est ce qui a laissé passer le défaut +pendant trois phases. Il faut vérifier les scripts que `index.html` référence réellement. + +--- + +## Le modèle : trois portées, et tout en découle + +C'est la notion structurante du projet. Chaque donnée appartient à l'une des trois, et +s'y tenir décide de tout le reste — ce qu'on filtre, ce qu'on affiche, ce qu'on cache. + +| Portée | Ce que c'est | Filtre-t-on dessus ? | +|---|---|---| +| **Commune** | Catalogue, auteurs, séries, revues, thèmes, **prêts** | **Jamais.** Un livre absent l'est pour tout le monde | +| **Personnelle** | Statut de lecture, liste d'envies, bibliographies masquées | **Toujours**, sur l'appelant, sans exception | +| **Trace** | `AjoutePar` | Jamais — c'est une information, pas une frontière | + +Le contraste le plus instructif est celui du **statut de lecture** et du **prêt**. Deux +membres du foyer lisent le même exemplaire à leur rythme : le statut est personnel. Mais +un livre sorti l'est pour tous, et n'importe qui doit pouvoir noter son retour : le prêt +est commun. Les points d'entrée des prêts ne reçoivent donc **aucune identité** — pour +qu'on ne puisse pas s'en servir par inadvertance. + +### Cinq tables qui refusent de fusionner + +`Livre`, `Revue`, `Serie`, `LivreSouhaite`, `RevueSouhaitee`. Le même argument revient +quatre fois, et il porte sur un invariant, pas sur le confort : + +> Logée dans `Livres`, une revue — ou un souhait — entrerait **mécaniquement** dans le +> catalogue, les compteurs, la détection de doublons, les séries et les bibliographies. +> Il faudrait ajouter « et qui n'est pas une revue » à **chaque** lecture. +> **Un invariant qu'on réécrit partout finit par être oublié quelque part.** + +Le prix est réel et se paie là où les listes se rejoignent : affichage, export `.txt`, +export `.csv`, instantané hors-ligne, renormalisation. C'est là que le sujet se rate +s'il est bâclé. + +### Deux astuces SQLite qui portent des règles métier + +- **Index uniques partiels.** `UNIQUE(LivreId) WHERE DateRetour IS NULL` garantit un seul + prêt ouvert par livre, tout en laissant l'historique accumuler autant de prêts clos + qu'il veut. Même mécanique pour la place d'un tome dans une série, et pour l'ISSN. +- **Colonnes normalisées.** SQLite ne sait pas ignorer les accents — `NOCASE` ne couvre + que l'ASCII, et il n'y a pas d'`unaccent()`. La forme mise à plat est donc **stockée**, + et la recherche y compare un terme passé par exactement la même fonction. + ⚠️ Toute écriture doit les recalculer. + +### ⚠️ Deux NULL sont distincts pour SQLite + +C'est le piège qui se repose **à chaque table personnelle**. `AuteurNormalise` et +`NumeroNormalise` sont `NOT NULL` avec un défaut vide : sinon « Médor, sans numéro » +s'ajouterait autant de fois qu'on cliquerait, l'index unique ne voyant que des valeurs +distinctes. + +### ⚠️ Les migrations sont fusionnées + +Une seule migration `InitialCreate` depuis la sortie du projet. **Aucune base antérieure +à la 0.5.0 n'est migrable** — voir [installer.md](installer.md). + +--- + +## Le serveur : enrichir une fiche depuis un code-barres + +C'est la fonction qui justifiait un projet sur mesure — les deux solutions existantes +échouaient dessus, l'une par dépendance à Google Books, l'autre par un bug d'auteur non +récupéré. + +``` +EAN-13 lu ──► préfixe 977 ? ──oui──► périodique : ISSN, titre BnF, fiche revue + │non + ▼ + BnF SRU · bib.isbn en ISBN-13 ──trouvé──► notice + │rien + ▼ + BnF SRU · bib.isbn en ISBN-10 ──trouvé──► notice + │rien + ▼ + OpenLibrary /isbn → /works → /authors ──► notice + │rien + ▼ + saisie manuelle, toujours accessible +``` + +### ⚠️ Les deux formes d'ISBN ne sont pas une précaution + +La BnF indexe l'ISBN **tel qu'imprimé**. Un livre d'avant 2007 ne porte qu'un ISBN-10 et +est donc **introuvable par son ISBN-13** — alors qu'un scanner lit toujours un EAN-13. +Mesuré : *Germinal* en poche donne 0 notice en 13, **3 en 10**. Sans la conversion, on +perd le fonds ancien, c'est-à-dire l'essentiel d'une bibliothèque constituée. + +- **BnF d'abord, OpenLibrary ensuite**, et pas l'inverse : la collection est francophone, + et le dépôt légal garantit structurellement la meilleure couverture. OpenLibrary est + riche sur l'anglophone, lacunaire sur le français. +- **Google Books est écarté** — préférence explicite, et clé d'API obligatoire. +- **Les couvertures viennent toujours d'OpenLibrary**, quelle que soit la source des + métadonnées : le Dublin Core BnF n'en fournit aucune. Le `?default=false` est + indispensable — sans lui on reçoit un placeholder au lieu d'un 404 franc. +- ⚠️ **Ne jamais valider une URL de couverture par un HEAD** avant de l'afficher : + OpenLibrary rend des 502 intermittents, on supprimerait au hasard des couvertures + existantes. + +### Le relais de couvertures est une surface SSRF + +`GET /api/couvertures` existe parce que le cache hors-ligne lit les octets par `fetch()`, +ce qui exige du CORS — or une URL de couverture collée à la main n'en a pas. Le relais +rend l'image de même origine. + +**Deux verrous, et aucun ne suffit seul.** L'URL doit **déjà être en base** (égalité +exacte), *et* la connexion ne s'ouvre que vers une adresse publiquement routable. + +⚠️ Le garde agit dans `ConnectCallback`, **pas avant la requête** : résoudre le nom puis +laisser `HttpClient` résoudre à nouveau laisserait passer un **DNS rebinding**. C'est +aussi ce qui permet de **suivre** les redirections en sécurité — et il le faut, +`covers.openlibrary.org` en fait deux avant d'aboutir sur `archive.org`. + +⚠️ **Tout refus répond 404**, sans distinguer les cas : autrement le point d'entrée serait +un **oracle** révélant quelles URL l'application connaît, et quelles adresses répondent +depuis le serveur. + +--- + +## Le client : consulter d'abord, modifier ensuite + +Une règle traverse les 24 écrans : **le mode est dans l'URL**, jamais dans un booléen +interne. `/livres/3` consulte, `/livres/3/edition` modifie. L'écran d'édition devient +partageable, et le bouton « retour » du navigateur une sortie naturelle. + +### ⚠️ Le piège Blazor qui va avec + +Les deux routes visent le même composant avec le même paramètre : passer de l'une à +l'autre ne change **aucun paramètre**, et le routeur ne redessine rien. L'URL changeait, +l'écran restait — alors qu'un chargement direct fonctionnait, ce qui masque le défaut. +D'où l'abonnement à `LocationChanged`, présent sur la fiche livre, la série et la revue. + +### Ce qui reste manipulable sans entrer en édition + +Deux exceptions assumées, parce qu'elles ne touchent pas à la *fiche* : le **statut de +lecture** (personnel, l'action la plus fréquente de l'application) et **prêter / marquer +rendu** (commun, un geste du quotidien). La **suppression**, elle, n'existe qu'en édition. + +### Trois règles d'affichage qui reviennent partout + +- **On n'affiche que ce qui a été choisi.** Le format n'apparaît que pour les ebooks + (un livre est physique par défaut), le type de document que s'il n'est pas + `NonPrecise`, les rôles d'auteurs qu'à partir de deux auteurs. Sans cela, chaque ligne + porterait « Non précisé ». +- **On grise, on ne masque jamais** — un faux négatif se voit et se corrige, une ligne + masquée à tort est invisible. Seule exception, explicite : le masquage manuel d'une + œuvre bibliographique, qui est personnel et compté à l'écran. +- **Hors-ligne, on désactive avec un motif, on ne masque pas.** Un bouton disparu est + indiscernable d'une fonction supprimée : l'utilisateur conclurait à une régression au + lieu d'apprendre que c'est le réseau. + +### ⚠️ Deux pièges Blazor payés cher + +**Une règle CSS scopée n'atteint pas ce que rend un composant enfant.** L'attribut de +portée `b-xxxx` n'est posé que sur le balisage du composant courant : `.menu-lien` ne +touchait pas les `` rendus par `NavLink`, et le menu restait en liens bruts. Les +règles du menu vivent donc en feuille **globale** — la navigation est ce qui doit le +moins pouvoir tomber. + +**Un composant qui écrit dans un objet prêté par son parent doit le prévenir.** Sinon +tout ce que le parent calcule à partir de cet objet — bouton désactivé, compteur, +message — reste figé sur l'état d'avant. Le symptôme trompe : on cherche une validation +fautive là où il n'y a qu'un rendu manquant. + +--- + +## Le hors-ligne : consultation seule, et neuf instantanés + +Le besoin réel est de consulter la bibliothèque **déjà enregistrée** sans réseau. Pas +d'écriture, pas de file d'attente, pas de résolution de conflits. + +### ⚠️ « Les données sont déjà en local » est faux + +Vrai au sens *serveur* seulement. Le code tourne dans le navigateur, SQLite vit sur le +serveur, et le service worker met en cache les **assets**, pas les réponses de l'API. +Sans travail explicite, l'application se lance hors-ligne et affiche **une liste vide**. + +D'où **IndexedDB** plutôt que le cache du service worker : un cache de réponses HTTP ne +restituerait que les URL déjà visitées, donc *une recherche jamais tapée en ligne ne +rendrait rien*. Ici l'instantané porte le catalogue **entier**, et la recherche se refait +localement. + +**Un instantané par vue**, jamais par requête : `catalogue`, `auteurs`, `prets-en-cours`, +`utilisateur`, `souhaits`, `series`, `revues`, `souhaits-revues`, `version`. + +⚠️ **Toute vue nouvelle doit en recevoir un**, faute de quoi son écran est mort +hors-ligne — c'est exactement ce qui était arrivé à la liste d'envies, et le symptôme +(un 404 en plein écran) ne désignait pas la cause. + +### ⚠️ `navigator.onLine` ne vaut que par sa négation + +« Faux » est fiable, « vrai » ne prouve rien — portail captif, serveur arrêté, Wi-Fi sans +Internet. D'où deux notions : `EnLigne` (ce que l'interface affiche) et `TenterLeReseau` +(faut-il tenter un appel). + +**Quand la panne vient du serveur, aucun événement `online` ne viendra jamais** : sans +cette distinction, l'application restait bloquée sur le cache jusqu'au rechargement. + +Et **deux événements distincts**, sinon boucle infinie : un rechargement écrirait un +instantané → nouvel événement → nouveau rechargement. + +### Le filtre existe en deux exemplaires, et c'est assumé + +`FiltreLivres` (serveur, sur `IQueryable`, colonnes normalisées déjà calculées) et +`FiltreLivresLocal` (navigateur, sur des DTO qui n'en portent pas) **ne peuvent pas être +le même code**. Le garde-fou est un test qui **confronte les deux** sur le même jeu de +données et 17 jeux de critères. + +⚠️ **Toute nouvelle dimension de filtre s'ajoute aux deux, et au test.** Une divergence +silencieuse serait pire qu'un cache absent : l'utilisateur conclurait que le livre n'est +pas dans sa bibliothèque. + +--- + +## Le déploiement : SSO par en-têtes, et une frontière dure + +MaBibli n'a **aucune connexion propre**. nginx authentifie via le portail YunoHost, +SSOwat injecte `YNH_USER`, l'application se contente de lire. OIDC a été écarté : +YunoHost n'en documente aucun pour les apps. + +### ⚠️ L'écoute sur 127.0.0.1 est une frontière de sécurité, pas un réglage + +L'application fait confiance à `YNH_USER` **parce que** SSOwat l'écrase à chaque requête +traversant nginx. Un service joignable directement permettrait à quiconque de forger +`YNH_USER` et de contourner tout le portail. Ne jamais élargir `ASPNETCORE_URLS`. + +| Contrainte | Pourquoi elle ne se négocie pas | +|---|---| +| **Domaine entier**, pas de sous-chemin | `` et les empreintes du service worker sont figés **à la compilation**, et rien n'est compilé sur le serveur | +| **HTTPS** | Le scan caméra n'existe qu'en contexte sécurisé. Tester depuis une IP locale échouera *toujours* | +| **x86_64** | Le publish visé est `linux-x64` ; déclarer d'autres architectures promettrait des archives qui n'existent pas | +| **Dépôt public** | `ynh_setup_source` télécharge sans jeton, et un dépôt privé répond **404 — pas 403**, symptôme identique à « la release n'existe pas » | + +### Deux pièges systemd invisibles hors d'un vrai serveur + +- `Environment=` **découpe sur les espaces** : sans guillemets, la ligne définit deux + variables au lieu d'une, et le service meurt sur un `ArgumentException`. +- `ProtectHome=yes` masque `/home`, où vit le `data_dir` de YunoHost — + `ReadWritePaths` ne perce pas cette protection. Il faut `tmpfs` + `BindPaths`. + +Ni l'un ni l'autre ne peut sortir d'un `dotnet run` : ils tiennent au gestionnaire de +services. C'est ce qui justifie de tester l'installation réelle, pas seulement le binaire. + +--- + +## La méthode, qui est peut-être le vrai acquis + +Le projet a une manière de trancher, et elle a été payée plusieurs fois : + +- **Mesurer avant de tailler.** Le menu de choix de version paraissait le meilleur + candidat à la coupe ; les neuf montées de version ont montré trois sauts mineurs. Il + sert une fois sur trois. +- **La vitesse ne dit rien du taux de réussite.** Un décodeur de code-barres a été + blanchi sur des mesures de *vitesse* alors qu'il *ratait* des codes. Pour juger un + décodeur, il faut mesurer **ce qu'il lit**, pas ce qu'il coûte. +- **Une source intermittente produit exactement les symptômes d'une source lacunaire.** + Un auteur cru absent de la BnF y avait 85 notices ; c'était un délai dépassé, vu deux + fois et pris pour deux problèmes. +- **Le rendu ne se vérifie pas à l'œil.** Un débordement de 8 px n'était visible que par + `scrollWidth`, et un 404 intermittent que par un chronométrage. +- **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. + +**Le corollaire, tenu jusqu'ici** : quand un raisonnement s'est révélé faux, il est resté +écrit, avec sa correction. C'est ce qui empêche de le reconduire — et trois cas montrent +qu'il aurait été reconduit sans cette trace. diff --git a/docs/installer.md b/docs/installer.md new file mode 100644 index 0000000..4e4e00a --- /dev/null +++ b/docs/installer.md @@ -0,0 +1,369 @@ +# Installer MaBibli + +Deux voies. **Seule la première apporte une authentification** — voir l'avertissement de +la section « Ailleurs que sur YunoHost ». + +| | | +|---|---| +| [Le paquet YunoHost](#le-paquet-yunohost) · [Contraintes](#contraintes-permanentes-du-paquet--ce-ne-sont-pas-des-tâches) | ce qu'il contient, ce qui ne se négocie pas | +| [Première mise en production](#a-première-mise-en-production) | pas à pas, sur un serveur neuf | +| [Désinstaller](#c-désinstaller) | avec ou sans les données | +| [Ailleurs que sur YunoHost](#installer-ailleurs-que-sur-yunohost-synology-nas-vps) | Synology, NAS, VPS — sans authentification | + +Pour **monter de version**, voir [publier-une-version.md](publier-une-version.md). +Pour comprendre le code, voir [architecture.md](architecture.md). + +## ⚠️ Depuis la 0.5.0, aucune base antérieure n'est migrable + +Les quinze migrations EF Core ont été **fondues en une seule** avant la sortie du projet. +Une installation existante porte dans `__EFMigrationsHistory` les quinze anciens noms : +au démarrage, EF Core verrait `InitialCreate` non appliquée et tenterait de créer des +tables déjà présentes. **La mise à jour échouerait, et son retour arrière aussi.** + +Sur un serveur déjà installé, la seule voie est donc : + +```bash +sudo yunohost app remove mabibli --purge +``` + +puis une installation neuve, **avec perte du catalogue**. + +⚠️ **Sans le `--purge`, `data_dir` survit** et l'ancienne base revient avec son historique +incompatible — le symptôme serait alors incompréhensible. Une réinstallation qui semble +propre repartirait sur des données qui ne le sont pas. + +⚠️ Cela ne concerne **que** les installations antérieures à la 0.5.0. Les montées de +version ultérieures fonctionnent normalement. + +## Le paquet YunoHost + +MaBibli se déploie via un **second dépôt**, [`mabibli_ynh`](https://forge.example.org/mabibli_ynh), +qui **ne contient aucun code C#** : il porte le manifeste, les configurations nginx et +systemd, et les scripts d'installation. Toute la documentation, elle, vit ici — ce +fichier est le point d'entrée unique du projet. + +### Contenu + +| Fichier | Rôle | +|---|---| +| `manifest.toml` | Identité, version, URL de l'archive et son `sha256`, ressources (utilisateur système, répertoires, port, permissions) | +| `conf/systemd.service` | Unité du service — écoute sur `127.0.0.1`, base dans le répertoire de données, durcissement | +| `conf/nginx.conf` | Reverse proxy et intégration SSOwat | +| `scripts/_common.sh` | Variables partagées et sauvegarde/restauration cohérente de la base SQLite | +| `scripts/install` `remove` `upgrade` `backup` `restore` | Cycle de vie de l'application | +| `build/publier.sh` | **Seul script de publication** : version, archive, manifeste, commit et push du paquet, tag et push du code. `--archive-seule` compile et archive sans rien publier, pour reprendre après un échec | +| `doc/DESCRIPTION.md` `doc/ADMIN.md` | Textes affichés par YunoHost lui-même (catalogue et interface d'administration) — ils doivent rester dans ce dépôt | + +### Points de conception + +**Le service n'écoute que sur `127.0.0.1`.** L'application déduit l'identité de l'en-tête +`YNH_USER` injecté par SSOwat ; cet en-tête n'est digne de confiance que si nginx est le seul +chemin d'accès. Un service exposé sur le réseau permettrait de forger `YNH_USER` et de +contourner le portail. La contrainte est écrite dans `mabibli_ynh/conf/systemd.service` (`ASPNETCORE_URLS`) +et le port n'est pas ouvert au pare-feu. + +Le packaging s'inspire de [radarr_ynh](https://github.com/YunoHost-Apps/radarr_ynh), +autre application .NET packagée sans Docker : c'est de là que vient le déploiement +self-contained, qui évite d'installer `dotnet-runtime` côté système. + +**Aucune compilation sur le serveur.** Le publish est *self-contained* : il embarque son propre +runtime .NET, donc aucun paquet `dotnet-runtime` n'est nécessaire. `ynh_setup_source` vérifie le +`sha256` de l'archive avant de la déployer. + +**Les données survivent aux mises à jour.** La base SQLite vit dans le répertoire de données, pas +à côté du binaire ; `upgrade` ne remplace que le répertoire d'installation. + +**La sauvegarde passe par l'API de sauvegarde en ligne de SQLite.** La base est en mode WAL : +copier le seul fichier `.db` d'une base active peut ne rien sauvegarder du tout. Voir +`mabibli_ynh/doc/ADMIN.md`. + +## Contraintes permanentes du paquet — ce ne sont pas des tâches + +### Le dépôt du code doit rester public + +`ynh_setup_source` télécharge **sans jeton**. Sur un dépôt privé, Gitea répond +**404 et non 403** à un anonyme : le symptôme est identique à « la release n'existe +pas », ce qui envoie chercher au mauvais endroit. Ne pas mettre de jeton dans l'URL +du manifeste — il serait lisible sur le serveur. + +### L'application exige un domaine entier, pas un sous-chemin + +MaBibli s'installe sur `mabibli.mondomaine.tld`, **pas** sur `mondomaine.tld/mabibli`. + +Deux éléments sont figés **à la compilation** du client Blazor WebAssembly : la balise +`` de `index.html`, et les empreintes d'intégrité de +`service-worker-assets.js`. Les réécrire sur le serveur casserait le service worker, +donc le mode hors-ligne — et rien n'est recompilé sur le serveur, c'est tout l'intérêt +du self-contained. Le paquet déclare donc l'application en `full_domain`. + +Lever cette contrainte demanderait une archive **par chemin d'installation**, ou une +compilation sur le serveur. Les deux annulent le bénéfice du self-contained. + +### `install_dir` finit par appartenir à l'application, pas à root + +Le manifeste demande `owner = "root:rwx"` pour que le service ne puisse pas réécrire +ses binaires, mais le helper `_ynh_apply_default_permissions` repasse derrière avec un +`chown -R mabibli:mabibli`. L'intention tient quand même, portée par +`ProtectSystem=strict` dans l'unité systemd : tout est en lecture seule sauf +`ReadWritePaths=`, qui ne liste que le `data_dir`. + +⚠️ Ne pas retirer `ProtectSystem=strict` en croyant que la propriété des fichiers +protège encore. + +--- + +## Mettre en production + +Deux marches à suivre, éprouvées sur un serveur réel le 2026-08-18 : la **première mise +en production**, puis la **montée de version**. `publier.sh` (ci-dessus) enchaîne les +étapes courantes ; ce qui suit les détaille une à une, pour les comprendre et pour les +cas qu'il ne couvre pas. + +``` +machine de dev Gitea serveur YunoHost +────────────── ───── ──────────────── +publier.sh + → archive .tar.gz ────────► release du dépôt `mabibli` + → manifest.toml ────────► dépôt `mabibli_ynh` ──────► yunohost app install + (url + sha256) (vérifie le sha256) +``` + +### A. Première mise en production + +#### A.1 Préalables, une seule fois + +- Le dépôt du code (`mabibli`) est **public** sur Gitea. `ynh_setup_source` télécharge + sans jeton ; sur un dépôt privé Gitea répond 404 — indiscernable d'une release + absente. +- Le domaine dédié existe côté YunoHost. MaBibli s'installe sur un **domaine entier** + (`mabibli.mondomaine.tld`), jamais sur un sous-chemin : + +```bash +sudo yunohost domain add mabibli.mondomaine.tld +``` + +- Le SDK .NET est installé sur la machine de développement (`dotnet --version`). + +#### A.2 Produire l'archive, le manifeste et le tag + +Depuis `mabibli_ynh`, avec le dépôt du code à côté (`../mabibli`) : + +```bash +./build/publier.sh --version 0.1.0 +``` + +Le script compile en `Release` self-contained, **vérifie le publish** (binaire présent, +`wwwroot/` embarqué, toutes les ressources d'`index.html` réellement sur disque, aucun +placeholder d'empreinte non substitué), produit l'archive de façon reproductible, en +calcule le `sha256`, réécrit `version`, `amd64.url` et `amd64.sha256` dans +`manifest.toml`, **commite et pousse le paquet**, puis **tague et pousse le code**. + +À contrôler dans sa sortie : la ligne `binaire, wwwroot et ressources d'index.html : OK`, +le `sha256` affiché — c'est celui que le manifeste porte désormais — et les deux `✓` de +la vérification côté distant. + +Il ne reste alors que A.3. Les étapes A.4 et suivantes décrivent ce que le script vient +de faire, pour le cas où l'on reprend à la main après un échec en cours de route +(`--archive-seule` s'arrête après l'archive, sans toucher au manifeste ni à git). + +#### A.3 Déposer la release + +Le tag `v0.1.0` est déjà poussé par A.2 (`git tag v0.1.0 && git push origin main --tags` +si l'on reprend à la main). + +**Dans l'interface Gitea** : créer la release `v0.1.0` sur le dépôt `mabibli`, et y +téléverser `build/dist/mabibli-0.1.0-linux-x64.tar.gz`. + +Vérification, à faire **sans être authentifié** (autre navigateur, ou `curl` comme +ci-dessous) : + +```bash +curl -fsSLI "https://forge.example.org/mabibli/releases/download/v0.1.0/mabibli-0.1.0-linux-x64.tar.gz" | head -1 +``` + +Attendu : `HTTP/2 200`. Un 404 signifie soit que la release n'est pas déposée, soit que +le dépôt est privé — les deux se ressemblent, commencer par vérifier la visibilité. + +#### A.4 Le paquet, sur Gitea + +⚠️ **L'étape la plus facile à oublier quand elle est manuelle** — et celle qui a fait +échouer la mise à jour du 2026-08-21. `yunohost app install ` lit le manifeste +**depuis Gitea**, jamais la copie locale : un manifeste corrigé mais non poussé n'existe +pas pour le serveur. A.2 s'en charge et vérifie l'arrivée par `git ls-remote` ; en +reprise à la main : + +```bash +cd ../mabibli_ynh && git add -A && git commit -m "Publier la version 0.1.0" && git push +``` + +Le contrôle qui tranche, quel que soit le chemin suivi — c'est le manifeste **tel que le +serveur le lira** : + +```bash +curl -fsSL "https://forge.example.org/mabibli_ynh/raw/branch/main/manifest.toml" | grep amd64 +``` + +#### A.5 Installer + +```bash +sudo yunohost app install https://forge.example.org/mabibli_ynh --debug +``` + +YunoHost demande le domaine (celui créé en A.1) et le groupe autorisé (`all_users`). + +**Ce qu'il faut voir passer :** `Prefetching asset main` sur la bonne URL, puis +l'installation des fichiers, nginx, systemd, et enfin `The service mabibli has correctly +executed the action start` — le script attend la ligne `Application started` du journal, +ce qui fait échouer franchement l'installation si les migrations EF Core ne passent pas. + +#### A.6 Vérifier + +```bash +sudo ss -tlnp | grep -i mabibli +``` + +Doit montrer **`127.0.0.1:` uniquement**. C'est une frontière de sécurité : sur +`0.0.0.0`, n'importe qui sur le réseau pourrait forger l'en-tête `YNH_USER` et se faire +passer pour un membre du foyer. + +```bash +sudo ls -l /home/yunohost.app/mabibli +``` + +`mabibli.db` doit exister : les migrations se sont appliquées seules au premier +démarrage. (Pas de `-wal` ni `-shm` au repos, c'est normal — SQLite fait un checkpoint à +la fermeture de la dernière connexion.) + +Enfin, dans un navigateur sur `https://mabibli.mondomaine.tld` : le portail authentifie, +et l'application affiche **ton** nom d'utilisateur YunoHost — pas « anonyme ». C'est le +seul contrôle qui éprouve réellement l'intégration SSO. + +--- + +### C. Désinstaller + +```bash +sudo yunohost app remove mabibli +``` + +Retire le service, la conf nginx, la permission SSO, l'utilisateur système, le port et +`/var/www/mabibli`. **`/home/yunohost.app/mabibli` survit**, donc la bibliothèque aussi : +une désinstallation faite trop vite ne doit pas être irréversible. + +⚠️ Corollaire à connaître en phase d'essai : réinstaller après un `remove` sans purge +**retrouve l'ancienne base**. Ce n'est pas une installation vierge, même si tout le +reste est neuf. + +Pour tout effacer, données comprises : + +```bash +sudo yunohost app remove mabibli --purge +``` + +Contrôle qu'il ne reste rien : + +```bash +systemctl status mabibli; sudo ls -d /var/www/mabibli /home/yunohost.app/mabibli 2>&1; getent passwd mabibli +``` + +Les trois doivent être négatifs. Le domaine, lui, reste déclaré dans YunoHost. + +--- + +## Installer ailleurs que sur YunoHost (Synology, NAS, VPS) + +Le déploiement de référence reste le paquet `mabibli_ynh` : c'est lui qui est éprouvé, et +c'est le seul qui apporte une **authentification**. Ce qui suit sert aux hébergements qui +n'ont pas de portail SSO. `Dockerfile`, `.dockerignore` et `compose.yaml`, à la racine de +ce dépôt, existent pour ça. + +L'application n'est qu'**un processus et un fichier SQLite** : `MaBibli.Api` sert lui-même +le client Blazor. Il n'y a donc rien à orchestrer. + +### ⚠️ Hors YunoHost, il n'y a AUCUNE authentification + +C'est le point à comprendre avant tout le reste, et il ne se contourne pas par la +configuration. `FournisseurUtilisateurSsowat` lit l'en-tête `YNH_USER` que le portail +injecte ; sans portail, il n'y a pas d'identité, et l'application **ne s'en invente pas**. + +Conséquence exacte, vérifiée en exécution : + +| | Sans identité | +|---|---| +| Catalogue, auteurs, séries, revues, prêts (**communs** au foyer) | fonctionnent | +| Statuts de lecture, listes d'envies (**personnels**) | refusés — `400 « Impossible d'ajouter une envie sans savoir à qui elle appartient. »` | + +Deux façons de rendre une identité, **aucune des deux n'authentifie qui que ce soit** : + +- `Identite__UtilisateurSimule` en variable d'environnement. C'est le mécanisme prévu pour + le développement, mais c'est de la configuration ordinaire : elle vaut aussi en + production. Mono-utilisateur. +- l'en-tête `YNH_USER` injecté par le proxy inversé (DSM : *Portail des applications > + Proxy inversé > En-tête personnalisé*). Même niveau de sécurité, mais si un proxy + authentifiant est ajouté un jour (Authelia, authentik), il n'y a plus qu'à lui faire + recopier son `Remote-User` vers `YNH_USER` : l'application n'a rien à changer. + +⚠️ Dans les deux cas, **quiconque atteint le port EST cet utilisateur**. Le port ne doit +donc être publié que sur la boucle locale (`127.0.0.1:8080:8080`), le proxy inversé restant +le seul chemin d'accès. C'est la transposition exacte de la contrainte d'écoute que le +paquet YunoHost pose comme dure. + +### Trois prérequis qui ne se négocient pas + +- **Une machine x86_64.** Le publish visé est `linux-x64` : les NAS ARM (les modèles « j » + notamment) demanderaient de republier en `linux-arm64`, ce qui n'est pas éprouvé ici. +- **HTTPS**, sans quoi le scan du code-barres ne s'ouvrira **jamais** : la caméra exige un + contexte sécurisé. Sur Synology, cela veut dire proxy inversé + certificat Let's Encrypt + sur un nom DDNS (`mabibli.xxx.synology.me`). +- **Un nom d'hôte entier, pas un sous-chemin** — `mabibli.xxx.synology.me`, jamais + `nas.xxx.synology.me/mabibli`. La raison est la même que pour le `full_domain` de + YunoHost, et elle est développée plus bas : `` et les empreintes du + service worker sont figés à la compilation. + +### La marche à suivre + +```bash +git clone mabibli && cd mabibli +# éditer compose.yaml : nom d'utilisateur, chemin du volume +docker compose up -d --build +``` + +Sur Synology, le même `compose.yaml` se colle dans *Container Manager > Projet*, en +remplaçant `./donnees` par un chemin réel (`/volume1/docker/mabibli`). + +Les migrations EF Core s'appliquent seules au premier démarrage : la base se crée dans le +volume, et c'est **le volume seul** qui la fait survivre au remplacement de l'image. + +### Ce qu'il faut savoir avant de s'y mettre + +- **La sauvegarde ne se fait pas en copiant le `.db`.** SQLite tourne en mode WAL : copier + le seul fichier d'une base active peut ne rien sauvegarder du tout. Il faut arrêter le + conteneur, ou passer par `sqlite3 … ".backup"` — c'est ce que fait le paquet YunoHost, et + la raison est écrite dans `mabibli_ynh/doc/ADMIN.md`. +- **Il n'y a pas de mécanisme de mise à jour.** Ni `yunohost app upgrade`, ni release, ni + vérification de `sha256` : à chaque version, `git pull` puis `docker compose up -d + --build`. Les données ne bougent pas, elles sont dans le volume. +- **L'image est construite depuis les sources, pas depuis une release.** Elle ne porte donc + aucun numéro de version, et « À propos » affiche honnêtement « version de développement ». + Pour un vrai numéro, passer `-p:Version=X.Y.Z -p:MaBibliDateBuild=…` au `dotnet publish` + du `Dockerfile` — voir `CLAUDE.md`, « Y — l'application dit sa version ». + +### Vérifié en exécution le 2026-08-21 + +Image construite et démarrée localement, base neuve dans un volume monté : + +| Cas | Résultat | +|---|---| +| Migrations au premier démarrage | appliquées, `mabibli.db` créé dans le volume | +| `GET /` et `_framework/blazor.webassembly.js` | **200**, `` intact | +| `GET /api/moi` avec `Identite__UtilisateurSimule` | `{"identifiant":"prenom","simule":true}` | +| `GET /api/moi` sans rien | `{"identifiant":null,"affichage":"inconnu"}` | +| `GET /api/moi` avec en-tête `YNH_USER: camille` | `{"identifiant":"camille","simule":false}` | +| `POST /api/souhaits` sans identité | **400**, message lisible | + +⚠️ **Ce qui n'a PAS été vérifié** : l'installation sur un Synology réel, le proxy inversé de +DSM et le certificat. Ce sont des gestes de DSM, pas du code — mais personne ne les a +éprouvés ici. + +--- + diff --git a/docs/publier-une-version.md b/docs/publier-une-version.md new file mode 100644 index 0000000..398b644 --- /dev/null +++ b/docs/publier-une-version.md @@ -0,0 +1,319 @@ +# Publier une version de MaBibli + +Pour vous, qui sortez les versions. Pour installer, voir +[installer.md](installer.md) ; pour comprendre le code, +[architecture.md](architecture.md). + +| | | +|---|---| +| [Avant de publier ailleurs](#avant-de-publier-ce-projet-quelque-part) | l'URL de dépôt, à configurer une fois | +| [Compiler](#compilation) | en développement, et le publish self-contained | +| [La chaîne de publication](#publication) | ce que `publier.sh` fait, et ce qu'il refuse | +| [Monter de version](#b-monter-de-version) | pas à pas | + +## Avant de publier ce projet quelque part + +Les URL de dépôt sont un **placeholder volontaire**, `https://forge.example.org/mabibli` +(le domaine `example.org` est réservé à la documentation par la RFC 2606, donc +définitivement injoignable). Rien ne fonctionnera tant qu'il n'est pas remplacé. + +**Une seule ligne commande tout le reste** — `depot_code`, en tête de +`mabibli_ynh/build/publier.sh`. Elle produit les URL de release, celles d'archive, et le +message de fin. Pour un essai ponctuel, `--depot-code https://…` fait la même chose sans +modifier le fichier. + +⚠️ **`publier.sh` refuse de publier tant que le placeholder est en place**, et il refuse +*avant* de compiler. C'est délibéré : publier avec une URL morte écrirait dans +`manifest.toml` une `amd64.url` que personne ne peut télécharger, et l'échec surviendrait +bien plus tard, à l'installation, sous la forme d'un `ynh_setup_source` en erreur — sans +rapport apparent avec la cause. + +Restent à changer à la main, une fois seulement : + +| Fichier | Quoi | +|---|---| +| `mabibli_ynh/manifest.toml` | `code = "…"`, et `maintainers` | +| `mabibli_ynh/conf/systemd.service` | `Documentation=…` | +| `mabibli_ynh/doc/ADMIN.md` | le lien de bas de page | +| `mabibli_ynh/README.md`, ce fichier | les liens et les commandes d'exemple | +| `MaBibli.Client/Pages/APropos.razor` | `Depot`, `Site`, et l'adresse de contact | + +⚠️ **`amd64.url` dans le manifeste n'est PAS à changer à la main** : `publier.sh` la +réécrit à chaque publication. La corriger donnerait l'illusion que c'est fait alors que la +source du problème est ailleurs. + +⚠️ **Le dépôt du code doit être PUBLIC.** `ynh_setup_source` télécharge sans jeton, et +Gitea comme GitHub répondent **404** — pas 403 — à un anonyme sur un dépôt privé. Le +symptôme est alors rigoureusement identique à « la release n'existe pas ». Contrôle qui +tranche, hors session authentifiée : `curl -fsSLI ""`. + +## Compilation + +Prérequis : le SDK **.NET 10** (`dotnet --version`). + +### En développement + +```bash +dotnet build +dotnet test +``` + +Pour lancer l'application en local (l'API sert aussi le client Blazor compilé) : + +```bash +dotnet run --project MaBibli.Api +``` + +### Publication self-contained (ce que le serveur reçoit réellement) + +Le serveur YunoHost ne compile jamais : il télécharge une archive déjà publiée et la +déploie telle quelle (voir « Publication » ci-dessous). Pour produire ce même artefact +en local, à des fins de vérification : + +```bash +dotnet publish MaBibli.Api --configuration Release --runtime linux-x64 --self-contained +``` + +`MaBibli.Api` référence `MaBibli.Client` : le client Blazor WebAssembly est compilé au +passage et atterrit dans `wwwroot/` du publish. Un seul projet à publier suffit donc +pour les trois (`MaBibli.Client`, `MaBibli.Shared`, `MaBibli.Api`). `--self-contained` +embarque le runtime .NET dans le dossier produit : aucun `dotnet-runtime` n'est requis +côté serveur. + +Pour un hébergement **sans YunoHost** (Synology, VPS), voir « Installer ailleurs que sur +YunoHost » plus bas : le `Dockerfile` de la racine fait ce même publish, mais sans +`--self-contained` — dans un conteneur, l'image `aspnet` fournit déjà le runtime. + +⚠️ **Ne jamais compiler sur le serveur YunoHost lui-même** : ce serait imposer le SDK +complet à une machine qui n'en a pas besoin, pour une compilation lente. Voir +`CLAUDE.md`, section « Chaîne de publication ». + +## Publication + +⚠️ **`build/dist/` est ignoré par git**, et doit le rester : l'archive vit dans la +release du dépôt. Committée, elle ajouterait ~65 Mo d'historique **par version**, +définitivement — c'est arrivé une fois, à la 0.1.0. + +Le geste courant tient en une commande, `build/publier.sh` du dépôt du paquet : + +```bash +cd ../mabibli_ynh +./build/publier.sh +``` + +Il reprend la version déjà publiée (tag le plus élevé, recoupé avec `manifest.toml`), +propose l'incrément, puis enchaîne tout ce qui suit : archive, manifeste, commit et push +du paquet, tag et push du code, avec vérification côté distant. Il ne reste qu'à créer +la release dans Gitea et y téléverser l'archive — le script en rappelle l'URL. + +⚠️ **Il n'y a qu'un seul script, et c'est délibéré.** Un second, `publier-release.sh`, +a existé jusqu'au 2026-08-21 : il compilait, archivait et réécrivait le manifeste, à +charge pour l'appelant de committer. Son nom inspirait plus confiance que celui du vrai +point d'entrée, et lancé seul il produisait exactement la panne du 2026-08-21 — un +`manifest.toml` corrigé mais non poussé, et une release `v0.4.0` contenant en réalité six +commits de plus que son tag. Les deux scripts sont fondus ; ce que le second savait +faire subsiste sous la même commande : + +```bash +./build/publier.sh --archive-seule +``` + +Ce mode compile et archive, **sans toucher ni au manifeste ni à git** : il ne peut donc +rien laisser à moitié publié. + +⚠️ **À quoi il sert vraiment** : à **reprendre une publication à la main** quand +`publier.sh` a échoué en cours de route (voir B.3). Ce n'est pas « une brique pour une +CI » — c'était la justification écrite jusqu'au 2026-08-22, et elle désignait un usage +qui n'a jamais eu lieu, là où la reprise après échec, elle, est documentée pas à pas. + +⚠️ **Les contrôles ne sont PAS désactivés dans ce mode**, contrairement à ce qui a été +vrai jusqu'au 2026-08-22 : ils s'exécutent tous, et **avertissent** au lieu de refuser, +puisque rien n'est publié et que rien ne peut donc mentir. Le dépôt sale, en +particulier, n'était alors pas signalé du tout — l'archive contenait en silence du +travail non committé. + +Ce script compile ce dépôt (`mabibli`) en `Release` self-contained, **vérifie** le +résultat (binaire présent, `wwwroot/` embarqué, toutes les ressources d'`index.html` +réellement sur disque, aucun placeholder d'empreinte non substitué), produit une +archive `.tar.gz` reproductible, calcule son `sha256`, et met à jour `version`, +`amd64.url` et `amd64.sha256` dans `mabibli_ynh/manifest.toml` — sauf en +`--archive-seule`. + +Il injecte au passage, dans l'assembly, la **version** (`-p:Version`) et un +**horodatage de build** (`-p:MaBibliDateBuild`), que la page « À propos » de +l'application affiche via `GET /api/version`. + +⚠️ **L'horodatage est le témoin de l'injection** : sans lui, l'application se déclare +« version de développement » plutôt que d'afficher le `1.0.0` que le SDK .NET pose par +défaut. Ce numéro-là se lirait comme une vraie version alors qu'il ne désigne rien — or +c'est précisément la valeur qu'on va chercher pour diagnostiquer un appareil dont le +cache est dépareillé. Un `dotnet build` local n'annonce donc aucune version, et c'est +voulu. + +⚠️ YunoHost lit le manifeste **depuis Gitea**, jamais une copie locale : un manifeste +corrigé mais non poussé n'existe pas pour le serveur. C'est précisément ce que +`publier.sh` empêche d'oublier. + +Il refuse par ailleurs de publier dans deux cas, **avant de compiler** : + +| Refus | Ce qu'il évite | +|---|---| +| le tag `vX.Y.Z` existe déjà | une release qui ne contient pas le code que son tag désigne — arrivé le 2026-08-21, six commits d'écart, sans que rien ne le signale | +| le dépôt du code a des modifications non committées | une archive contenant du travail que le tag, lui, ne contient pas | + +Dans les deux cas il nomme le commit en cause et rappelle `--patch`. ⚠️ En +`--archive-seule`, les mêmes contrôles **avertissent** au lieu de refuser — le constat +s'affiche, le conseil « choisissez un numéro libre » non : en reprise, le tag visé +est justement celui qu'on veut retrouver. + +**La marche à suivre complète et éprouvée**, pour les cas que `publier.sh` ne couvre +pas — première mise en production, montée de version pas à pas, retour arrière, +désinstallation — est en « Mettre en production » ci-dessous. + + +--- + +## ⚠️ Monter de version depuis une installation antérieure à la 0.5.0 + +**Impossible** : les migrations ont été fondues en une seule, et l'historique de la base +ne correspond plus. Il faut `remove --purge` puis réinstaller, avec perte du catalogue — +voir [installer.md](installer.md). Ce qui suit vaut pour les montées **entre versions +0.5.0 et ultérieures**. + +### B. Monter de version + +#### B.1 Ce qui distingue une version applicative d'une révision de paquet + +| Ce qui change | Version | Nouvelle archive ? | +|---|---|---| +| Le code C# | `0.1.0` → `0.1.1~ynh1` | **oui** | +| Seulement le paquet (conf systemd/nginx, scripts) | `0.1.0~ynh1` → `0.1.0~ynh2` | non | + +Le second cas est le plus simple : bump du suffixe `~ynhN` **à la main** dans +`manifest.toml`, commit, push, puis directement l'étape B.5. L'archive et son `sha256` +ne bougent pas. (C'est ce qui a été fait pour `~ynh2` et `~ynh3`.) + +La suite décrit le premier cas. + +#### B.2 En une commande : `publier.sh` + +C'est le geste courant, décrit en « [Publication](#publication) » plus haut : il enchaîne +B.3, B.4 et B.5 (archive, manifeste, commit et push du paquet, tag et push du code), et +vérifie sur le distant que tout est arrivé. + +```bash +cd ../mabibli_ynh && ./build/publier.sh +``` + +La suite décrit ces mêmes étapes **à la main**, pour comprendre ce que le script fait et +pour les cas qu'il ne couvre pas : révision `~ynhN` seule, reprise après un échec en +cours de route. + +#### B.3 Compiler et vérifier la nouvelle version + +Le code est prêt et committé dans `mabibli`. Depuis `mabibli_ynh` : + +```bash +./build/publier.sh --archive-seule --patch +``` + +⚠️ **`--version` n'est plus obligatoire ici, depuis le 2026-08-22.** Le mode reprenait +alors la version de `manifest.toml` — c'est-à-dire celle **déjà publiée** — et +reproduisait donc 0.1.0 tant qu'on ne le corrigeait pas à la main. Les deux modes +calculent maintenant la version de la même façon : `--patch` propose 0.1.1, et +`--version` ne sert plus qu'à viser un autre numéro (par exemple celui d'un tag déjà +créé, si la publication a échoué après le tag). + +⚠️ **`--archive-seule` ne touche pas au manifeste** : `version` (avec son suffixe +`~ynh1` — une nouvelle version applicative repart toujours de 1), `amd64.url` et +`amd64.sha256` sont alors à réécrire à la main. C'est le prix de la marche à pied ; +`publier.sh` sans option le fait, et ne peut pas l'oublier. + +Note le `sha256` affiché — il ne sera plus jamais le même, même à code identique si les +dépendances bougent. + +#### B.4 Tag et release + +```bash +cd ../mabibli && git tag v0.1.1 && git push origin main --tags +``` + +Créer la release `v0.1.1` dans Gitea, y téléverser +`build/dist/mabibli-0.1.1-linux-x64.tar.gz`, puis vérifier sans authentification : + +```bash +curl -fsSLI "https://forge.example.org/mabibli/releases/download/v0.1.1/mabibli-0.1.1-linux-x64.tar.gz" | head -1 +``` + +#### B.5 Pousser le paquet + +```bash +cd ../mabibli_ynh && git add -A && git commit -m "Publier la version 0.1.1" && git push +``` + +#### B.6 Sauvegarder, puis mettre à jour + +YunoHost prend **lui-même** une sauvegarde de sécurité avant la mise à jour +(`mabibli-pre-upgrade1`), mais elle **ne contient pas le répertoire de données** +(`BACKUP_CORE_ONLY`) : elle sert à restaurer l'application, pas la bibliothèque. Prendre +une sauvegarde complète reste donc utile avant une version qui touche au schéma : + +```bash +sudo yunohost backup create --apps mabibli +``` + +```bash +sudo yunohost app upgrade mabibli -u https://forge.example.org/mabibli_ynh --debug +``` + +⚠️ **L'option `-u` n'est pas facultative ici.** MaBibli n'est pas dans le catalogue +officiel : sans elle, YunoHost ne sait pas où retrouver le paquet et refuse d'emblée — +« mabibli is not in the catalog (anymore?) », puis « No apps can be upgraded ». Rien +n'est cassé pour autant, la commande n'a simplement pas commencé. C'est la **même URL** +qu'à l'installation, celle du dépôt `_ynh`, jamais celle de l'archive. + +(Ajouter `--force` seulement pour réappliquer une version identique, par exemple en +mise au point du paquet.) + +Le script arrête le service **avant** de remplacer les binaires — deux processus sur la +même base SQLite pendant une migration est exactement ce qu'il faut éviter — puis +attend `Application started`, avec un délai de 120 s : une migration sur base remplie +prend plus de temps que la création d'un schéma vide. + +#### B.7 Vérifier après mise à jour + +```bash +sudo systemctl status mabibli --no-pager && sudo journalctl -u mabibli -n 30 --no-pager +``` + +Puis, dans le navigateur, le contrôle qui compte vraiment : **les livres sont toujours +là**. C'est ce qui valide que la base vit bien dans le répertoire de données et non à +côté du binaire — `upgrade` remplace intégralement `/var/www/mabibli`. + +Vider le cache du navigateur n'est pas nécessaire : le service worker compare les +empreintes et propose « Mettre à jour ». Sur mobile, un onglet resté ouvert peut +retarder la bascule — c'est précisément ce que le bandeau de mise à jour sert à +débloquer. + +#### B.8 Si la mise à jour échoue + +YunoHost restaure automatiquement la sauvegarde de sécurité quand le script échoue. Si +le service démarre mais que l'application se comporte mal, revenir en arrière à la main : + +```bash +sudo yunohost backup list +``` + +```bash +sudo yunohost app remove mabibli --purge +``` + +```bash +sudo yunohost backup restore --apps mabibli +``` + +⚠️ `--purge` efface le répertoire de données. Ne le faire qu'avec une archive contenant +la bibliothèque sous la main — celle de B.5, pas `mabibli-pre-upgrade1`. + +--- +