mathieuandClaude Opus 5 1b47828c11 Remonte le type de document, colle le menu, et fais du retour une remontée
Cinq retours d'usage du 2026-08-21 qui ne touchent que l'interface (lots Q, R,
S, T et V d'IDEES.md). Aucune migration, aucune entité modifiée.

Q — L'ordre des champs du formulaire livre est une décision, pas une mise en
page : type de document, thèmes, auteurs, rôles, puis le reste. Le type
commande la suite de la saisie — les rôles ne se posent que là — et il fallait
descendre tout le formulaire pour dire « c'est une BD », c'est-à-dire après
avoir saisi ce qui en dépend. Rien n'est présélectionné : « non précisé » ne
prétend toujours rien, et les rôles gardent leur règle actée (deux auteurs au
moins, BD ou non). Des tests verrouillent la préservation des rôles à chaque
frappe, que ce réordonnancement ne doit pas entamer.

R — Bandeau et menu passent dans un même conteneur collant : sur PC la rangée
des six destinations défilait avec la page et devenait inatteignable au bas
d'une longue liste. Un seul conteneur, et non deux éléments collants
superposés, qui auraient obligé à écrire en dur la hauteur d'un bandeau qui
varie avec la pastille hors-ligne et le nom d'utilisateur. Sur téléphone le
menu déployé occupe l'écran entier : sous le bandeau, il partageait l'écran
avec la liste qu'on quittait. Trois portes de sortie s'ajoutent à la fermeture
déjà en place sur LocationChanged — croix, Échap (le calque prend le focus à
l'ouverture, comme celui d'agrandissement des couvertures) et clic hors des
liens. Le plein écran est explicitement annulé au-delà de 40 rem, sans quoi un
menu ouvert au doigt puis une fenêtre agrandie laisseraient un calque sans
bascule pour le refermer. Toutes les règles du menu restent en feuille globale.

S — ⚠️ Le retour du bandeau devient une remontée hiérarchique d'un cran de
route, ce qui RENVERSE la décision actée le 2026-08-20 (« le retour passe par
l'historique du navigateur, jamais par une destination calculée »). Le motif
d'alors reste vrai — un même écran s'atteint par plusieurs chemins — mais
l'historique remonte aussi les allers-retours (filtre, ordre, édition) et l'on
cliquait cinq fois sans quitter le même écran. La parenté est une table
explicite et testable, pas un découpage naïf d'URL : toutes les routes n'ont
pas la forme d'une arborescence, /auteurs/{id}/bibliographie remontant à la
liste des auteurs, dont il n'existe pas de fiche. Le garde-fou « ne jamais
sortir de l'application » y vit désormais aussi : la fonction rend toujours un
chemin interne, là où history.length ne disait pas ce qu'on croyait dans une
PWA standalone. js/navigation.js n'a plus d'utilisateur et disparaît.

T — Sur la fiche d'une série, « Modifier » et « Changer l'ordre » rejoignent la
ligne du titre et de l'avancement, groupés à droite comme la bibliographie et
la liste des auteurs le font déjà. Reléguées au bas de l'écran, les actions
d'une saga de vingt tomes ne se découvraient qu'après avoir déroulé la liste.
« Changer l'ordre » n'apparaît toujours qu'à partir de deux tomes ou deux
sous-séries.

V — Dans la liste des revues, toute la ligne ouvre la fiche, comme la carte
entière le fait au catalogue. Le titre reste un vrai lien — adresse, clavier,
clic-milieu — et son clic ne remonte pas jusqu'à la ligne, qui naviguerait une
seconde fois ; toute action posée un jour sur cette ligne devra faire de même.

⚠️ Aucune vérification en navigateur : le rendu de ces écrans repose sur la
compilation et la relecture. 552 tests au vert (506 avant ce lot).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:38:46 +02:00

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.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 (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

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_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 :
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.00.1.1~ynh1 oui
Seulement le paquet (conf systemd/nginx, scripts) 0.1.0~ynh10.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.

S
Description
No description provided
Readme AGPL-3.0
6 MiB
2026-08-23 17:36:06 +02:00
Languages
C# 67.8%
HTML 25.8%
CSS 4%
JavaScript 2.2%
Dockerfile 0.2%