# 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](#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 |

## 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

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

⚠️ **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`](https://git.akbar.nohost.me/mathieu/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 :

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

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 :

```bash
./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 :

```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

Depuis `mabibli_ynh`, avec le dépôt du code à côté (`../mabibli`) :

```bash
./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

```bash
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) :

```bash
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.

```bash
cd ../mabibli_ynh && git add -A && git commit -m "Publier la version 0.1.0" && git push
```

#### A.5 Installer

```bash
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

```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-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

```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://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

```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://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

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

---

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