Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MaBibli
Application de gestion de bibliothèque personnelle, self-hosted sur YunoHost.
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.
| Demande initiale · Contraintes techniques | ce que l'application doit faire, et avec quoi |
| Compilation | développer et produire l'artefact |
| Le paquet YunoHost | contenu du dépôt mabibli_ynh, points de conception |
| Publication | sortir une version, en une commande |
| Mettre en production | installer, mettre à jour, revenir en arrière, désinstaller |
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.mdpour 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 (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.mdpour 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
dotnet build
dotnet test
Pour lancer l'application en local (l'API sert aussi le client Blazor compilé) :
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 :
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.
⚠️ 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 ».
Le paquet YunoHost
MaBibli se déploie via un second dépôt, 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 |
Publie une version de bout en bout : version, archive, manifeste, commits, tag, push |
build/publier-release.sh |
Compile, archive, calcule le sha256 et met à jour manifest.toml |
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 :
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.
Sous le capot, c'est build/publier-release.sh qui produit l'artefact ; il s'utilise
seul quand on veut une archive sans rien publier :
./build/publier-release.sh --version 0.4.0 --no-manifest-update
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 avec
--no-manifest-update.
⚠️ 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.
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_sourceté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 :
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
Depuis mabibli_ynh, avec le dépôt du code à côté (../mabibli) :
./build/publier-release.sh
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, et réécrit version, amd64.url et amd64.sha256 dans
manifest.toml.
À contrôler dans sa sortie : la ligne binaire, wwwroot et ressources d'index.html : OK,
et le sha256 affiché — c'est celui que le manifeste porte désormais.
A.3 Déposer la release
cd ../mabibli && git tag v0.1.0 && git push origin main --tags
Puis, 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) :
curl -fsSLI "https://git.akbar.nohost.me/mathieu/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 Pousser le paquet
⚠️ L'étape la plus facile à oublier. 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.
cd ../mabibli_ynh && git add -A && git commit -m "Publier la version 0.1.0" && git push
A.5 Installer
sudo yunohost app install https://git.akbar.nohost.me/mathieu/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
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.
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 » 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é.
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 :
./build/publier-release.sh --version 0.1.1
⚠️ --version est obligatoire ici. Sans lui, le script déduit la version de
manifest.toml et reproduirait 0.1.0. Il remet aussi le suffixe à ~ynh1 : une
nouvelle version applicative repart toujours de 1.
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
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 :
curl -fsSLI "https://git.akbar.nohost.me/mathieu/mabibli/releases/download/v0.1.1/mabibli-0.1.1-linux-x64.tar.gz" | head -1
B.5 Pousser le paquet
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 :
sudo yunohost backup create --apps mabibli
sudo yunohost app upgrade mabibli -u https://git.akbar.nohost.me/mathieu/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
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 :
sudo yunohost backup list
sudo yunohost app remove mabibli --purge
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
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 :
sudo yunohost app remove mabibli --purge
Contrôle qu'il ne reste rien :
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.
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.