diff --git a/build/publier.sh b/build/publier.sh index 145f878..efb9221 100755 --- a/build/publier.sh +++ b/build/publier.sh @@ -36,12 +36,18 @@ set -euo pipefail racine_paquet="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" manifeste="$racine_paquet/manifest.toml" -source_dir="${MABIBLI_SOURCE_DIR:-$(cd "$racine_paquet/../mabibli" 2>/dev/null && pwd || true)}" -base_url="${MABIBLI_BASE_URL:-https://git.akbar.nohost.me/mathieu/mabibli/releases/download}" -url_releases="${MABIBLI_URL_RELEASES:-https://git.akbar.nohost.me/mathieu/mabibli/releases}" -output_dir="${MABIBLI_OUTPUT_DIR:-$racine_paquet/build/dist}" +# ⚠️ Chaque réglage a UNE seule façon de se donner : son option en ligne de commande. +# Cinq variables d'environnement jumelles (MABIBLI_SOURCE_DIR, MABIBLI_BASE_URL…) ont +# existé jusqu'au 2026-08-22, chacune doublant une option qu'elle ne faisait que +# répéter : l'aide en listait dix pour cinq réglages, et rien ne disait laquelle +# l'emportait sur l'autre. Ne pas les réintroduire pour une CI hypothétique — une CI +# passe des options aussi bien que des variables. +source_dir="$(cd "$racine_paquet/../mabibli" 2>/dev/null && pwd || true)" +base_url="https://git.akbar.nohost.me/mathieu/mabibli/releases/download" +url_releases="https://git.akbar.nohost.me/mathieu/mabibli/releases" +output_dir="$racine_paquet/build/dist" -version="${MABIBLI_VERSION:-}" +version="" increment="" sans_confirmation=0 archive_seule=0 @@ -58,14 +64,13 @@ Usage : publier.sh [options] --major Incrémenter le premier chiffre (0.3.0 -> 1.0.0) --oui, -y Ne rien demander (pour une CI) --archive-seule Compiler et archiver SEULEMENT : ni manifeste, ni commit, ni tag. - Pour une CI ou un essai ; ne publie rien et ne modifie aucun dépôt. + Pour REPRENDRE une publication à la main après un échec en cours + de route ; ne publie rien et ne modifie aucun dépôt. Les contrôles + restent les mêmes, mais avertissent au lieu de refuser. --source-dir CHEMIN Dépôt du code C# (défaut : ../mabibli) --base-url URL Racine des URL de release, sans le tag ni le fichier --output-dir CHEMIN Où déposer l'archive (défaut : build/dist) -h, --help Cette aide - -Variables d'environnement équivalentes : MABIBLI_SOURCE_DIR, MABIBLI_VERSION, -MABIBLI_BASE_URL, MABIBLI_OUTPUT_DIR, MABIBLI_URL_RELEASES. FIN } @@ -85,13 +90,38 @@ done echoerr() { echo "$@" >&2; } +# ⚠️ LE SEUL ENDROIT où `--archive-seule` assouplit quoi que ce soit. +# +# Les garde-fous ci-dessous protègent la *publication* : ils empêchent qu'une release +# contienne autre chose que le code que son tag désigne. En `--archive-seule`, rien n'est +# publié — ni manifeste, ni commit, ni tag — donc rien ne peut mentir, et un refus n'a +# plus lieu d'être. Il reste un avertissement, car ce qu'il signale est toujours vrai. +# +# Chaque garde-fou s'écrit donc UNE fois, sans savoir dans quel mode il tourne. C'est ce +# qui empêche d'assouplir par inadvertance un contrôle qui devait rester dur. +# ⚠️ Le CONSTAT et le CONSEIL sont séparés, et ce n'est pas de la cosmétique : le constat +# reste vrai dans les deux modes, le conseil non. « Choisissez un numéro libre » est le +# bon conseil pour une publication, et un mauvais conseil en reprise — où le tag visé est +# justement celui qu'on veut retrouver. +refuser() { # refuser [...] + local conseil="$1"; shift + if [ "$archive_seule" -eq 1 ]; then + printf ' ⚠ %s\n' "$@" + echo " (poursuite : --archive-seule ne publie rien)" + else + printf '%s\n' "$@" >&2 + [ -n "$conseil" ] && echoerr "$conseil" + exit 1 + fi +} + #================================================= # CONTRÔLES PRÉALABLES #================================================= if [ -z "$source_dir" ] || [ ! -d "$source_dir" ]; then echoerr "Dépôt du code introuvable : '${source_dir:-}'" - echoerr "Précisez-le avec --source-dir (ou MABIBLI_SOURCE_DIR)." + echoerr "Précisez-le avec --source-dir." exit 1 fi source_dir="$(cd "$source_dir" && pwd)" @@ -101,9 +131,13 @@ if [ ! -d "$source_dir/$projet" ]; then exit 1 fi -if [ "$archive_seule" -eq 0 ] && [ ! -d "$source_dir/.git" ]; then - echoerr "'$source_dir' n'est pas un dépôt git : impossible de taguer la release." - echoerr "Pour compiler sans rien publier : --archive-seule." +# ⚠️ Exigé dans les DEUX modes. `--archive-seule` sert à reprendre une publication à la +# main, jamais à compiler un arbre quelconque : sans dépôt, il ne saurait ni proposer une +# version, ni dire si l'archive correspond au code d'un tag. L'exiger ici plutôt qu'à +# quatre endroits plus bas est ce qui rend le reste du script inconditionnel. +if [ ! -d "$source_dir/.git" ]; then + echoerr "'$source_dir' n'est pas un dépôt git." + echoerr "publier.sh compile ce qu'un commit désigne : sans dépôt, rien ne le désigne." exit 1 fi @@ -120,9 +154,7 @@ fi # Le tag fait foi : c'est lui qui désigne le code publié. Le manifeste ne porte que la # dernière version *empaquetée*, qui peut être en retard si une publication a échoué en # cours de route. On prend le plus élevé des deux pour ne jamais proposer un numéro déjà pris. -version_tag="" -[ -d "$source_dir/.git" ] && - version_tag="$(git -C "$source_dir" tag --list 'v[0-9]*' --sort=-v:refname | head -1 | sed 's/^v//')" +version_tag="$(git -C "$source_dir" tag --list 'v[0-9]*' --sort=-v:refname | head -1 | sed 's/^v//')" version_manifeste="$(sed -n 's/^version *= *"\([^"~]*\).*"/\1/p' "$manifeste" | head -1)" precedente="$(printf '%s\n%s\n' "$version_tag" "$version_manifeste" \ @@ -137,53 +169,49 @@ suivante() { # suivante esac } -if [ "$archive_seule" -eq 1 ]; then - # Rien n'est publié : la version ne sert qu'à nommer l'archive et à alimenter - # `-p:Version`. Celle du manifeste convient, et on ne propose aucun incrément. - [ -z "$version" ] && version="$version_manifeste" - if [ -z "$version" ]; then - echoerr "Impossible de déduire la version depuis $manifeste ; utilisez --version." - exit 1 - fi -else - if [ -z "$precedente" ]; then - echoerr "Aucune version précédente trouvée (ni tag, ni manifest.toml) ; utilisez --version." +# ⚠️ Le calcul de la version est le MÊME dans les deux modes, et c'est délibéré. +# `--archive-seule` retombait auparavant sur la version du manifeste, c'est-à-dire sur +# celle **déjà publiée** : reprendre une publication à la main reproduisait donc le +# numéro précédent tant qu'on ne passait pas `--version`. Le README devait avertir de ce +# piège ; il n'existe plus. Une reprise après échec veut la version qu'on était en train +# de publier, et c'est exactement ce que l'incrément propose. +if [ -z "$precedente" ]; then + echoerr "Aucune version précédente trouvée (ni tag, ni manifest.toml) ; utilisez --version." + exit 1 +fi + +echo "Dernière version publiée : $precedente" +[ "$version_tag" != "$version_manifeste" ] && + echo " ⚠ tag v${version_tag:-—} et manifest.toml ${version_manifeste:-—} divergent" + +if [ -z "$version" ] && [ -n "$increment" ]; then + version="$(suivante "$precedente" "$increment")" +fi + +if [ -z "$version" ]; then + if [ "$sans_confirmation" -eq 1 ] || [ ! -t 0 ]; then + echoerr "Pas de terminal pour demander la version ; utilisez --version ou --patch/--minor/--major." exit 1 fi + echo + echo " 1) $(suivante "$precedente" patch) (correction)" + echo " 2) $(suivante "$precedente" minor) (nouveautés)" + echo " 3) $(suivante "$precedente" major) (rupture)" + echo " 4) autre — à saisir" + echo + read -rp "Version à publier [1] ? " choix + case "${choix:-1}" in + 1) version="$(suivante "$precedente" patch)" ;; + 2) version="$(suivante "$precedente" minor)" ;; + 3) version="$(suivante "$precedente" major)" ;; + 4) read -rp "Numéro de version : " version ;; + *) version="$choix" ;; # un numéro tapé directement passe aussi + esac +fi - echo "Dernière version publiée : $precedente" - [ "$version_tag" != "$version_manifeste" ] && - echo " ⚠ tag v${version_tag:-—} et manifest.toml ${version_manifeste:-—} divergent" - - if [ -z "$version" ] && [ -n "$increment" ]; then - version="$(suivante "$precedente" "$increment")" - fi - - if [ -z "$version" ]; then - if [ "$sans_confirmation" -eq 1 ] || [ ! -t 0 ]; then - echoerr "Pas de terminal pour demander la version ; utilisez --version ou --patch/--minor/--major." - exit 1 - fi - echo - echo " 1) $(suivante "$precedente" patch) (correction)" - echo " 2) $(suivante "$precedente" minor) (nouveautés)" - echo " 3) $(suivante "$precedente" major) (rupture)" - echo " 4) autre — à saisir" - echo - read -rp "Version à publier [1] ? " choix - case "${choix:-1}" in - 1) version="$(suivante "$precedente" patch)" ;; - 2) version="$(suivante "$precedente" minor)" ;; - 3) version="$(suivante "$precedente" major)" ;; - 4) read -rp "Numéro de version : " version ;; - *) version="$choix" ;; # un numéro tapé directement passe aussi - esac - fi - - if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echoerr "Version invalide : '$version' (attendu X.Y.Z)." - exit 1 - fi +if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echoerr "Version invalide : '$version' (attendu X.Y.Z)." + exit 1 fi #================================================= @@ -193,38 +221,38 @@ fi # ⚠️ Le garde-fou qui manquait le 2026-08-21. Recompiler une version dont le tag existe # déjà produit une archive qui ne correspond PAS au code que ce tag désigne : la release # se met alors à mentir, et l'écran « À propos » avec elle, sans que rien ne le signale. -if [ -d "$source_dir/.git" ] && git -C "$source_dir" rev-parse "v$version" >/dev/null 2>&1; then +if git -C "$source_dir" rev-parse "v$version" >/dev/null 2>&1; then commit_tag="$(git -C "$source_dir" rev-list -n1 "v$version")" commit_head="$(git -C "$source_dir" rev-parse HEAD)" - if [ "$archive_seule" -eq 1 ]; then - [ "$commit_tag" != "$commit_head" ] && - echo " ⚠ v$version désigne ${commit_tag:0:7}, HEAD est ${commit_head:0:7} : l'archive ne sera PAS le code du tag." + if [ "$commit_tag" != "$commit_head" ]; then + refuser "Choisissez un numéro libre (--patch)." \ + "Le tag v$version existe et désigne ${commit_tag:0:7} ; HEAD est ${commit_head:0:7}." \ + "L'archive ne serait donc PAS le code que ce tag désigne." else - echoerr "Le tag v$version existe déjà dans $source_dir (${commit_tag:0:7})." - [ "$commit_tag" != "$commit_head" ] && - echoerr "HEAD est ${commit_head:0:7} : republier sous ce numéro mettrait un autre code dans la release v$version." - echoerr "Choisissez un numéro libre (--patch), ou compilez sans publier (--archive-seule)." - exit 1 + refuser "Choisissez un numéro libre (--patch)." \ + "Le tag v$version existe déjà (${commit_tag:0:7}) : ce numéro est pris." fi fi # Le tag va désigner le HEAD du dépôt du code : un travail non committé ne serait donc # pas dans la release, alors même que l'archive, elle, le contiendrait. -if [ "$archive_seule" -eq 0 ] && [ -n "$(git -C "$source_dir" status --porcelain)" ]; then - echoerr "Le dépôt du code a des modifications non committées :" +# +# ⚠️ Ce contrôle était auparavant SAUTÉ en `--archive-seule`, qui produisait donc en +# silence une archive contenant du travail non committé. Il s'exécute désormais toujours, +# et se contente d'avertir quand rien n'est publié : c'est précisément l'écart qu'on veut +# voir quand on reprend une publication à la main. +if [ -n "$(git -C "$source_dir" status --porcelain)" ]; then git -C "$source_dir" status --short >&2 - exit 1 + refuser "" \ + "Le dépôt du code a des modifications non committées (ci-dessus)." \ + "Le tag désignera HEAD : ce travail ne serait pas dans la release." fi echo echo "=== À publier ===" echo " version : ${precedente:-—} -> $version" -if [ -d "$source_dir/.git" ]; then - branche="$(git -C "$source_dir" rev-parse --abbrev-ref HEAD)" - echo " code : $source_dir ($branche, $(git -C "$source_dir" rev-parse --short HEAD))" -else - echo " code : $source_dir" -fi +branche="$(git -C "$source_dir" rev-parse --abbrev-ref HEAD)" +echo " code : $source_dir ($branche, $(git -C "$source_dir" rev-parse --short HEAD))" echo " paquet : $racine_paquet" [ "$archive_seule" -eq 1 ] && echo " mode : --archive-seule (ni manifeste, ni commit, ni tag)" diff --git a/doc/ADMIN_fr.md b/doc/ADMIN_fr.md deleted file mode 100644 index 473f9dd..0000000 --- a/doc/ADMIN_fr.md +++ /dev/null @@ -1,82 +0,0 @@ -## Comment l'authentification fonctionne - -MaBibli n'a **pas de connexion propre**. L'identité vient entièrement du portail YunoHost : -nginx authentifie le visiteur, puis SSOwat injecte dans la requête les en-têtes `YNH_USER`, -`YNH_USER_EMAIL` et `YNH_USER_FULLNAME`, que l'application se contente de lire. - -Conséquences pratiques : - -- **Qui peut voir la bibliothèque se règle dans les permissions YunoHost** (`mabibli.main`), - pas dans l'application. -- La collection est **commune** à toutes les personnes autorisées. Chaque livre garde une trace - de qui l'a saisi, mais personne n'est cloisonné : c'est une bibliothèque de foyer. -- Les **statuts de lecture sont personnels** (chacun sa progression), les **prêts sont communs** - (un livre absent l'est pour tout le monde). -- Se déconnecter du portail YunoHost ne déconnecte pas nécessairement des applications : - chacune garde sa propre session. C'est une limite connue de YunoHost, pas de MaBibli. - -⚠️ **Ne pas exposer le port interne.** Le service écoute volontairement sur `127.0.0.1` -uniquement. L'application fait confiance à `YNH_USER` parce que SSOwat écrase cet en-tête à -chaque requête ; un service joignable directement permettrait à quiconque de forger -`YNH_USER` et de contourner le portail. Le port n'est pas ouvert au pare-feu, et -`ASPNETCORE_URLS` dans l'unité systemd ne doit jamais être élargi à `0.0.0.0` ou `*`. - -## Domaine entier obligatoire - -MaBibli s'installe **sur un domaine entier**, pas sous un sous-chemin (`/mabibli`). - -Le client est une application Blazor WebAssembly : son chemin de base et les empreintes -d'intégrité de son service worker sont figés **à la compilation**. Comme le paquet installe une -archive déjà compilée — et ne compile jamais rien sur le serveur — il n'existe pas de moyen -propre de les réécrire à l'installation. YunoHost refusera donc un changement d'URL vers un -sous-chemin. - -## Où vivent les données - -| Quoi | Où | -|---|---| -| Binaires et client web | `/var/www/mabibli` (appartient à `root`, l'application ne peut pas s'y écrire) | -| Base SQLite | `/home/yunohost.app/mabibli/mabibli.db` | -| Journaux | `journalctl -u mabibli` | - -La base est **délibérément séparée des binaires** : une mise à jour remplace intégralement -`/var/www/mabibli` sans jamais toucher aux données. Le schéma est migré automatiquement au -démarrage, il n'y a aucune commande à lancer après une mise à jour. - -## Sauvegarde - -La base tourne en mode **WAL**. C'est important pour qui voudrait bricoler une sauvegarde à la -main : à un instant donné, l'essentiel des données peut se trouver dans `mabibli.db-wal` et -**pas** dans `mabibli.db`. Mesuré sur une base fraîchement migrée, `mabibli.db` faisait 4 Ko — -et ne contenait **aucune table** — pendant que le fichier `-wal` en portait 205 Ko. - -Le script de sauvegarde du paquet ne copie donc pas les fichiers tels quels : il demande à -SQLite un instantané cohérent (`.backup`, l'API de sauvegarde en ligne), déposé à côté de la -base sous le nom `mabibli-instantane.db`. C'est ce fichier que la restauration remet en place, -en écartant au passage les `-wal` / `-shm` de l'archive, qui décrivaient l'état d'une autre -copie de la base. - -Le service **n'est pas arrêté** pendant la sauvegarde : l'API de sauvegarde en ligne garantit la -cohérence du fichier produit sans bloquer les lectures, et couper l'application à chaque -sauvegarde nocturne coûterait une indisponibilité pour rien. - -Pour une sauvegarde manuelle, la bonne commande est donc : - -```bash -sqlite3 /home/yunohost.app/mabibli/mabibli.db ".backup '/quelque/part/mabibli.db'" -``` - -et surtout pas un `cp` du seul fichier `.db`. - -## Le scan du code-barres exige HTTPS - -L'accès à la caméra n'est autorisé par les navigateurs que dans un contexte sécurisé. En -production, le certificat Let's Encrypt de YunoHost suffit. En revanche, joindre le serveur par -son IP locale (`http://192.168.x.x`) fera **toujours** échouer le scan : ce n'est pas un -contexte sécurisé. La saisie manuelle de l'ISBN reste disponible dans tous les cas. - -## Accès sortant nécessaire - -Le serveur doit pouvoir joindre `catalogue.bnf.fr` et `openlibrary.org` en HTTPS pour -pré-remplir les fiches à partir d'un ISBN. Sans accès sortant, l'application fonctionne, mais -toute saisie devient manuelle. diff --git a/doc/DESCRIPTION_fr.md b/doc/DESCRIPTION_fr.md deleted file mode 100644 index 324b2f4..0000000 --- a/doc/DESCRIPTION_fr.md +++ /dev/null @@ -1,9 +0,0 @@ -MaBibli est une application de gestion de bibliothèque personnelle, pensée pour un foyer. - -- **Catalogue** des livres physiques et des ebooks (fiches uniquement, aucun fichier n'est hébergé). -- **Prêts** : à qui le livre a été confié, depuis quand, et l'historique complet des prêts passés. -- **Scan ISBN** au code-barres depuis le téléphone, ou saisie manuelle, avec pré-remplissage automatique du titre, de l'auteur, de l'éditeur et de la couverture. -- **Statuts de lecture** personnels : chaque membre du foyer suit sa propre progression sur une collection commune. -- **Consultation hors-ligne** : l'application est une PWA installable, et la bibliothèque reste consultable sans réseau. - -Les métadonnées viennent du catalogue de la **BnF** puis d'**OpenLibrary**, deux sources libres et sans clé d'API. Aucune dépendance à Google Books. diff --git a/manifest.toml b/manifest.toml index 110c57b..db7ebc4 100644 --- a/manifest.toml +++ b/manifest.toml @@ -37,8 +37,33 @@ sso = true # ~170 Mo de publish self-contained (runtime .NET embarqué), plus la base et la marge # de décompression de l'archive. disk = "500M" + +# ⚠️ `ram.build` ne désigne AUCUNE compilation : rien n'est compilé sur le serveur, c'est +# tout l'intérêt du self-contained. Il couvre le téléchargement et la décompression de +# l'archive de 69 Mo. Ne pas aller chercher la valeur du côté d'un build .NET. ram.build = "50M" -ram.runtime = "200M" + +# Mesuré sur le serveur le 2026-08-22, service en marche : +# cat /sys/fs/cgroup/system.slice/mabibli.service/memory.peak -> 208 793 600 o +# soit 199,1 Mio de pic, contre 200M déclarés jusqu'ici : 0,4 % de marge, c'est-à-dire +# aucune. Porté à 256M. +# +# ⚠️ `systemctl show -p MemoryPeak` ne renvoie RIEN sur ce serveur — la commande réussit +# en silence et l'on croit lire un pic là où l'on ne lit rien. Passer par le cgroup. +# +# ⚠️ Le chiffre MAJORE le besoin : `memory.peak` compte le cache de fichiers, donc une +# part des 69 Mo de binaire mappé, récupérable sous pression — et le GC de .NET se serre +# sur une machine étroite. Mais YunoHost compare une déclaration statique à la RAM +# disponible, sans faire cette nuance. L'asymétrie tranche : un refus d'installation est +# visible et se contourne, un OOM survient plus tard, sous une opération lourde, sans que +# rien ne désigne la cause. +# +# ⚠️ Le pic n'est PAS le lookup ISBN, contrairement à l'intuition — une notice. C'est +# « Nouveautés » sur un auteur très réédité : jusqu'à dix pages de 100 notices SRU en +# parallèle, plus le catalogue entier chargé pour le rapprochement. Ce cas-là n'a pas +# encore été mesuré, et il croît avec la taille de la bibliothèque : à reprendre le jour +# où le fonds aura beaucoup grossi. +ram.runtime = "256M" [install]