Files
mabibli/README.md
T
mathieuandClaude Opus 5 498579f27b Dis à l'application quelle version elle est, et sous quelle licence
Page « À propos » (/a-propos) : manuel annoncé à venir, contact, site de
l'auteur, version publiée avec sa date de build, et licence AGPL v3 avec le
lien vers le dépôt — l'AGPL attend que les utilisateurs d'un service en réseau
puissent en obtenir la source, ce lien n'est donc pas un ornement.

L'application ne connaissait pas sa version : elle vivait dans le manifeste du
paquet et dans les tags git, jamais dans le binaire. `build/publier-release.sh`
pose désormais `-p:Version` et `-p:MaBibliDateBuild` ; l'API rend les deux par
`GET /api/version`.

⚠️ L'horodatage est le TÉMOIN de l'injection, et rien ne le calcule côté
MSBuild. Sans lui, la version lue serait le « 1.0.0 » que le SDK pose par
défaut : il se lirait comme une vraie version alors qu'il ne désigne rien, et
c'est exactement la valeur qu'on ira chercher pour diagnostiquer un appareil au
cache dépareillé. Mieux vaut ne rien annoncer — un binaire compilé à la main se
déclare « version de développement », ce qui est vrai.

Autres décisions :

- l'entrée du menu est DÉTACHÉE des six destinations, par un filet au-dessus
  sur téléphone et à gauche en rangée sur PC : « À propos » est une annexe, pas
  une septième destination. Ses règles vivent en feuille GLOBALE, comme tout le
  menu — une règle scopée n'atteint pas ce que rend un NavLink ;
- le « mailto: » reste un lien même hors-ligne : il ne charge aucune page et
  passe la main au client de messagerie, qui sait mettre un message en attente.
  Le site et le dépôt, eux, basculent en boutons désactivés portant leur motif,
  comme les liens d'export des envies ;
- la version est un sixième instantané hors-ligne : on la lit justement quand
  quelque chose ne va pas, et un appareil qu'on soupçonne est souvent celui qui
  n'a plus de réseau. L'écran dit alors que c'est la dernière version vue du
  serveur.

Le point de rupture de 40 rem reste identique dans les deux feuilles, et
/a-propos est inscrite dans la table de remontée des routes.

Vérifié en exécution, l'API lancée : sans injection
`{"numero":null,"publiee":false}`, avec `-p:Version=0.4.1
-p:MaBibliDateBuild=…` `{"numero":"0.4.1","publiee":true}`. Le rendu des écrans
n'a PAS été vérifié en navigateur. 618 tests au vert (605 avant).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 14:50:34 +02:00

504 lines
21 KiB
Markdown

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