Decoupe la documentation par audience, et corrige trois defauts

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 <noreply@anthropic.com>
This commit is contained in:
mathieu
2026-08-22 13:30:42 +02:00
co-authored by Claude Opus 5
parent 9e31e147b5
commit b6c4dbaa08
4 changed files with 1063 additions and 669 deletions
+64 -669
View File
@@ -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 "<url de l'archive>"`.
## 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&nbsp;: 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 <url>` 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:<port>` 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 <nom-de-l-archive> --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 : `<base href="/">` et les empreintes du
service worker sont figés à la compilation.
### La marche à suivre
```bash
git clone <ce dépôt> 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**, `<base href="/">` 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
`<base href="/">` 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.
+311
View File
@@ -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 `<a>` 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 | `<base href="/">` 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.
+369
View File
@@ -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
`<base href="/">` 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 <url>` 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:<port>` 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 : `<base href="/">` et les empreintes du
service worker sont figés à la compilation.
### La marche à suivre
```bash
git clone <ce dépôt> 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**, `<base href="/">` 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.
---
+319
View File
@@ -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 "<url de l'archive>"`.
## 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&nbsp;: 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 <nom-de-l-archive> --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`.
---