Sortir du mode hors-ligne quand la panne venait du serveur, et documenter
Constate en essai : quand c'est le serveur qui tombe, navigator.onLine n'a jamais change, donc aucun evenement « online » ne viendra jamais. Les lectures court-circuitaient sur EnLigne et ne retentaient plus rien : l'application restait sur le cache jusqu'au rechargement de la page. EtatReseau distingue donc deux notions : EnLigne (ce que l'interface affiche et ce qui active les boutons) et TenterLeReseau (faut-il tenter un appel, vrai des que le navigateur a une connexion, meme apres un echec). Verifie : serveur arrete puis redemarre sans aucun evenement « online », la navigation suivante retire le bandeau, reactive les actions et fait apparaitre un livre ajoute cote serveur pendant la coupure. CLAUDE.md : architecture retenue, ce qui a ete observe reseau coupe, pourquoi le filtre existe en deux exemplaires, ce que le hors-ligne ne couvre pas, la chaine de mise a jour du service worker, et la cause etablie de son echec d'enregistrement dans le navigateur d'automatisation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,7 +43,7 @@ Application self-hosted de gestion de bibliothèque personnelle, à héberger su
|
|||||||
| Hébergement | YunoHost, installation **native** (pas Docker) | YunoHost déconseille Docker pour ses apps (moins fiable, plus lourd) ; installation native = meilleures perfs sur petit matériel |
|
| Hébergement | YunoHost, installation **native** (pas Docker) | YunoHost déconseille Docker pour ses apps (moins fiable, plus lourd) ; installation native = meilleures perfs sur petit matériel |
|
||||||
| Packaging YunoHost | S'inspirer de [`radarr_ynh`](https://github.com/YunoHost-Apps/radarr_ynh) | Radarr est aussi en .NET, packagé sans Docker sur YunoHost. Leur `manifest.toml` montre un déploiement **self-contained** (`dotnet publish -r linux-x64 --self-contained`), donc pas besoin d'installer `dotnet-runtime` via apt côté serveur — le binaire embarque son propre runtime |
|
| Packaging YunoHost | S'inspirer de [`radarr_ynh`](https://github.com/YunoHost-Apps/radarr_ynh) | Radarr est aussi en .NET, packagé sans Docker sur YunoHost. Leur `manifest.toml` montre un déploiement **self-contained** (`dotnet publish -r linux-x64 --self-contained`), donc pas besoin d'installer `dotnet-runtime` via apt côté serveur — le binaire embarque son propre runtime |
|
||||||
| Scan ISBN | **ZXing.Net** (C#, Apache 2.0) exécuté dans le WASM ; le JS ne fournit que les pixels caméra | Décodage en C#, réutilisable hors navigateur si le projet évolue en scanner de bibliothèque. Voir la section dédiée ci-dessous |
|
| Scan ISBN | **ZXing.Net** (C#, Apache 2.0) exécuté dans le WASM ; le JS ne fournit que les pixels caméra | Décodage en C#, réutilisable hors navigateur si le projet évolue en scanner de bibliothèque. Voir la section dédiée ci-dessous |
|
||||||
| Consultation hors-ligne | Cache local des données côté client (mécanisme à trancher) | Le besoin est de **consulter la bibliothèque existante** sans réseau, pas d'enrichir de nouveaux livres. Voir « Stratégie hors-ligne » |
|
| Consultation hors-ligne | **Instantanés JSON en IndexedDB**, lecture seule, implémenté le 2026-08-18 | Le besoin est de **consulter la bibliothèque existante** sans réseau, pas d'enrichir de nouveaux livres. Voir « Stratégie hors-ligne » |
|
||||||
|
|
||||||
## Scan du code-barres — ZXing.Net (décision actée)
|
## Scan du code-barres — ZXing.Net (décision actée)
|
||||||
|
|
||||||
@@ -152,6 +152,95 @@ public static class IsbnScanner
|
|||||||
- Hors-ligne, l'interface doit **désactiver explicitement** les actions d'écriture (ajout, édition, prêt) plutôt que de les laisser échouer silencieusement, et indiquer que les données affichées proviennent du cache.
|
- Hors-ligne, l'interface doit **désactiver explicitement** les actions d'écriture (ajout, édition, prêt) plutôt que de les laisser échouer silencieusement, et indiquer que les données affichées proviennent du cache.
|
||||||
- SQLite compilé en WASM côté client a été **écarté** : ne se justifierait que si l'écriture hors-ligne devenait nécessaire.
|
- SQLite compilé en WASM côté client a été **écarté** : ne se justifierait que si l'écriture hors-ligne devenait nécessaire.
|
||||||
|
|
||||||
|
### Ce qui a été implémenté le 2026-08-18
|
||||||
|
|
||||||
|
| Pièce | Rôle |
|
||||||
|
|---|---|
|
||||||
|
| `wwwroot/js/cache-hors-ligne.js` | IndexedDB (base `mabibli`, magasin `instantanes`) : ranger, relire, et signaler les bascules `online`/`offline`. Aucune logique métier |
|
||||||
|
| `Services/CacheHorsLigne.cs` | Sérialisation C# des instantanés ; **avale toute panne du stockage** (navigation privée, quota) — ne pas pouvoir cacher n'empêche pas de fonctionner en ligne |
|
||||||
|
| `Services/EtatReseau.cs` | État réseau visible + date de dernière synchronisation, avec deux événements distincts |
|
||||||
|
| `Services/FiltreLivresLocal.cs` | Recherche, filtres et tri hors-ligne sur l'instantané |
|
||||||
|
| `Services/ServiceLivresApi.cs` | Lectures avec repli sur le cache, écritures refusées |
|
||||||
|
|
||||||
|
**Quatre instantanés, un par vue de l'API**, jamais un par requête : `catalogue` (le catalogue
|
||||||
|
**entier**, sans filtre), `auteurs`, `prets-en-cours`, `utilisateur`. C'est exactement ce qui
|
||||||
|
justifie IndexedDB plutôt que le cache du service worker : un cache de réponses HTTP ne
|
||||||
|
restituerait que les URL déjà visitées, donc **une recherche jamais tapée en ligne ne rendrait
|
||||||
|
rien**. Vérifié en exécution — hors-ligne, chercher un auteur jamais affiché auparavant remonte
|
||||||
|
bien son livre.
|
||||||
|
|
||||||
|
L'instantané est réécrit à chaque lecture non filtrée réussie **et après chaque écriture**
|
||||||
|
(rafraîchissement en tâche de fond) : sans cela, une coupure juste après un ajout montrerait un
|
||||||
|
catalogue d'avant.
|
||||||
|
|
||||||
|
### Le filtre existe en deux exemplaires, et c'est assumé
|
||||||
|
|
||||||
|
`FiltreLivres` (serveur, sur `IQueryable<Livre>`, colonnes normalisées déjà calculées) et
|
||||||
|
`FiltreLivresLocal` (navigateur, sur des `LivreDto` qui n'en portent pas) **ne peuvent pas être
|
||||||
|
le même code**. La normalisation est refaite à la volée côté client — sans coût perceptible sur
|
||||||
|
une bibliothèque de foyer — mais par **les mêmes fonctions** (`NormalisationTexte`,
|
||||||
|
`RapprochementAuteurs.Cle`), et le tri est **ordinal** pour reproduire ce que fait SQLite sur une
|
||||||
|
colonne sans collation.
|
||||||
|
|
||||||
|
⚠️ Le garde-fou est un test qui **confronte les deux implémentations** sur le même jeu de données
|
||||||
|
et 17 jeux de critères (`FiltreLivresLocalTests`). Une divergence silencieuse serait pire qu'un
|
||||||
|
cache absent : l'utilisateur conclurait que le livre n'est pas dans sa bibliothèque.
|
||||||
|
|
||||||
|
### ⚠️ `navigator.onLine` ne suffit pas — le piège coûte le retour en ligne
|
||||||
|
|
||||||
|
`navigator.onLine` **ne vaut que par sa négation** : « faux » est fiable, « vrai » ne prouve rien
|
||||||
|
(portail captif, serveur arrêté, Wi-Fi sans Internet). D'où deux notions distinctes dans
|
||||||
|
`EtatReseau`, et il faut tenir les deux :
|
||||||
|
|
||||||
|
| Propriété | Sens | Usage |
|
||||||
|
|---|---|---|
|
||||||
|
| `EnLigne` | navigateur en ligne **et** dernier appel réussi | ce que l'interface affiche et ce qui active les boutons |
|
||||||
|
| `TenterLeReseau` | navigateur en ligne, **même si le dernier appel a échoué** | faut-il tenter un appel HTTP |
|
||||||
|
|
||||||
|
**Constaté en essai avant correction** : quand la panne vient du *serveur*, `navigator.onLine`
|
||||||
|
n'a jamais changé, donc aucun événement `online` ne viendra jamais — et une lecture qui
|
||||||
|
court-circuitait sur `EnLigne` ne retentait plus rien. L'application restait bloquée sur le cache
|
||||||
|
**jusqu'au rechargement de la page**. Le prix de la correction est d'une requête qui échoue par
|
||||||
|
lecture tant que le serveur est absent : elle échoue vite, et l'affichage retombe sur le cache.
|
||||||
|
|
||||||
|
Vérifié après correction : serveur arrêté puis redémarré, **sans aucun événement `online`**, la
|
||||||
|
navigation suivante a fait disparaître le bandeau, réactivé les actions et fait apparaître un
|
||||||
|
livre ajouté côté serveur pendant la coupure.
|
||||||
|
|
||||||
|
### ⚠️ Deux événements, sinon la boucle infinie
|
||||||
|
|
||||||
|
`EtatReseau` expose `Change` (bascule en ligne ↔ hors ligne) **et** `SynchroChange` (nouvelle date
|
||||||
|
de synchronisation). Les écrans se rechargent sur `Change` uniquement. Avec un événement unique,
|
||||||
|
un rechargement écrirait un instantané → nouvel événement → nouveau rechargement, sans fin.
|
||||||
|
|
||||||
|
### Ce que le hors-ligne ne couvre pas, volontairement
|
||||||
|
|
||||||
|
- **L'historique des prêts d'un livre** (`GET /api/livres/{id}/prets`) : une requête par livre pour
|
||||||
|
une information rarement consultée. En revanche l'état *courant* vient de `LivreDto.PreteA`,
|
||||||
|
donc de l'instantané — c'est lui qui répond à « où est ce livre ? », la seule question qui se
|
||||||
|
pose devant l'étagère. Le composant **dit** que l'historique est indisponible plutôt que
|
||||||
|
d'afficher une liste vide, qui se lirait « jamais prêté ».
|
||||||
|
- **Les rapprochements d'auteurs** : une liste de décisions à prendre, or aucune décision ne peut
|
||||||
|
être enregistrée hors-ligne.
|
||||||
|
- **Le lookup ISBN et le scan** : ils interrogent la BnF et OpenLibrary. L'écran le dit
|
||||||
|
explicitement au lieu de laisser expirer un délai d'attente incompréhensible.
|
||||||
|
- Une fiche absente de l'instantané affiche « pas dans les données enregistrées sur cet
|
||||||
|
appareil », **pas** « n'existe plus » : hors-ligne, les deux ne se distinguent pas.
|
||||||
|
|
||||||
|
### Ce qui a été observé, réseau coupé
|
||||||
|
|
||||||
|
Éprouvé sur un `publish Release` servi par l'API, en coupant réellement l'accès à `/api/*`
|
||||||
|
(même origine, donc même IndexedDB) puis en **rechargeant** la page :
|
||||||
|
|
||||||
|
- démarrage à froid : les 9 livres s'affichent, bandeau « Hors ligne. Données enregistrées
|
||||||
|
aujourd'hui à 13:59. » — instantanés écrits à 13:59:32, page rechargée à 14:00 ;
|
||||||
|
- recherche sur tout le fonds : `saint-exupery` (auteur jamais affiché auparavant), `bete` → « La
|
||||||
|
Bête humaine », `emile` → les Zola, `zola emile` (ordre inversé) → les mêmes ;
|
||||||
|
- filtres format et statut opérants sur l'instantané ;
|
||||||
|
- « Ajouter par ISBN », « Saisie manuelle », « Éditer », les quatre boutons de statut, « Prêter »,
|
||||||
|
« Rendu » et « Ajouter au catalogue » : **désactivés**, chacun portant sa raison ;
|
||||||
|
- « Prêts en cours » et « Auteurs » servis depuis le cache, le nom d'utilisateur aussi.
|
||||||
|
|
||||||
## Sources de données ISBN — point d'attention important
|
## Sources de données ISBN — point d'attention important
|
||||||
|
|
||||||
**Ne pas dépendre d'une seule source, et éviter Google Books si possible** (préférence explicite de l'utilisateur : pas de dépendance à Google).
|
**Ne pas dépendre d'une seule source, et éviter Google Books si possible** (préférence explicite de l'utilisateur : pas de dépendance à Google).
|
||||||
@@ -290,6 +379,68 @@ Le cache-busting reste assuré par le service worker, qui compare les empreintes
|
|||||||
|
|
||||||
**Piège de diagnostic à conserver** : vérifier que `/` renvoie 200 ne prouve rien — c'est ce qui a fait passer le défaut inaperçu à la phase 1. Il faut vérifier les scripts que `index.html` référence **réellement**, ou charger la page dans un navigateur.
|
**Piège de diagnostic à conserver** : vérifier que `/` renvoie 200 ne prouve rien — c'est ce qui a fait passer le défaut inaperçu à la phase 1. Il faut vérifier les scripts que `index.html` référence **réellement**, ou charger la page dans un navigateur.
|
||||||
|
|
||||||
|
### Le service worker est le SEUL cache-busting du projet — conséquences
|
||||||
|
|
||||||
|
Les empreintes étant désactivées, `blazor.webassembly.js` et `dotnet.js` portent des noms
|
||||||
|
stables : rien d'autre que le service worker n'empêche de servir éternellement une version
|
||||||
|
périmée. Vérifié que la chaîne tient, sans navigateur, en publiant deux fois avec une seule
|
||||||
|
ligne de différence dans `app.css` :
|
||||||
|
|
||||||
|
| | Publication A | Publication B |
|
||||||
|
|---|---|---|
|
||||||
|
| `service-worker-assets.js` → `version` | `Oc+bE5e+` | `jBwHvKfd` |
|
||||||
|
| Première ligne de `service-worker.js` | `/* Manifest version: Oc+bE5e+ */` | `/* Manifest version: jBwHvKfd */` |
|
||||||
|
|
||||||
|
Le point important est la **seconde ligne** : le SDK écrit la version dans le corps même de
|
||||||
|
`service-worker.js`. Le navigateur compare ce fichier **octet à octet** à chaque vérification de
|
||||||
|
mise à jour — il n'a donc pas à deviner que `service-worker-assets.js` a changé. Nouveau worker →
|
||||||
|
nouveau nom de cache (`offline-cache-{version}`) → tous les assets refetchés. Un changement de
|
||||||
|
code C# suffit aussi (empreintes des `.wasm`), constaté : `Oc+bE5e+` → `wCvu+Chi`.
|
||||||
|
|
||||||
|
**Ce que le mécanisme d'origine ne réglait pas** : un nouveau worker *attend* que **tous** les
|
||||||
|
onglets de l'application soient fermés. Sur mobile, un onglet oublié fige la mise à jour sans que
|
||||||
|
personne comprenne pourquoi. D'où `wwwroot/js/mise-a-jour.js` :
|
||||||
|
|
||||||
|
- il **vérifie le support avant d'appeler `navigator.serviceWorker`** — absent en contexte non
|
||||||
|
sécurisé (http sur une IP locale), où l'appel direct levait une `TypeError` ;
|
||||||
|
- il **journalise un échec d'enregistrement** au lieu de l'avaler (c'est ce qui a permis
|
||||||
|
d'élucider le point ci-dessous) ;
|
||||||
|
- il appelle `registration.update()` à chaque chargement, et affiche un bandeau « Mettre à jour »
|
||||||
|
quand une version est prête ; le clic envoie `SKIP_WAITING` au worker en attente, qui appelle
|
||||||
|
`self.skipWaiting()` (ajouté à `service-worker.published.js`), puis `controllerchange` recharge.
|
||||||
|
|
||||||
|
⚠️ Le rechargement sur `controllerchange` est **conditionné à un clic** : cet événement survient
|
||||||
|
aussi à la toute première installation, et recharger à ce moment-là serait un clignotement
|
||||||
|
inexplicable. Ne pas retirer le drapeau.
|
||||||
|
|
||||||
|
### ⚠️ Le service worker ne s'enregistre pas dans le navigateur d'automatisation — c'est l'environnement
|
||||||
|
|
||||||
|
Symptôme constaté depuis la phase 3, cause établie le 2026-08-18. **Ne pas repartir en chasse au
|
||||||
|
bug de configuration PWA** : les quatre observations ci-dessous vont toutes dans le même sens.
|
||||||
|
|
||||||
|
1. `navigator.serviceWorker.getRegistrations()` renvoie `[]`, et `register()` échoue en
|
||||||
|
`TypeError: … An unknown error occurred when fetching the script.`
|
||||||
|
2. Un `fetch('/service-worker.js')` **depuis la même page** renvoie `200 text/javascript`,
|
||||||
|
3 335 octets. Le fichier est donc bien servi.
|
||||||
|
3. **Toutes** les URL échouent identiquement, y compris `/index.html` — or un HTML *récupéré*
|
||||||
|
échouerait avec une erreur de type MIME, pas avec « fetching the script ». L'échec est donc
|
||||||
|
**avant** la requête.
|
||||||
|
4. Décisif : journalisation `Microsoft.AspNetCore` en `Information`, puis un `fetch` et un
|
||||||
|
`register` sur la même URL portant chacun un repère distinct. Le serveur journalise
|
||||||
|
`Request starting … ?repere=fetchB` et **rien du tout** pour `?repere=registerB`. La requête
|
||||||
|
d'enregistrement **ne quitte jamais le navigateur**.
|
||||||
|
|
||||||
|
Le navigateur en question n'est pas un Chrome ordinaire : `navigator.userAgent` indique
|
||||||
|
`Claude/1.30096.1 Chrome/148 Electron/42.7.0` — un hôte Electron, dont la couche d'interception
|
||||||
|
réseau ne sert pas les requêtes de script de service worker.
|
||||||
|
|
||||||
|
**Conséquence à assumer** : le démarrage hors-ligne *complet* (coquille HTML/WASM servie par le
|
||||||
|
service worker) n'est **pas vérifiable ici**. Ce qui a été vérifié pour de bon, c'est tout le
|
||||||
|
reste — coquille servie par un serveur statique sur la **même origine**, API réellement
|
||||||
|
injoignable, et l'application repart de son cache IndexedDB. À confirmer dans un navigateur
|
||||||
|
ordinaire : charger l'application, vérifier dans les outils de développement (Application →
|
||||||
|
Service Workers) que le worker est `activated`, cocher « Offline », puis recharger.
|
||||||
|
|
||||||
### Deux dépôts distincts
|
### Deux dépôts distincts
|
||||||
|
|
||||||
| Dépôt | Contenu | Rôle |
|
| Dépôt | Contenu | Rôle |
|
||||||
@@ -544,7 +695,7 @@ Conclusion : aucune des deux solutions existantes ne coche toutes les cases (pr
|
|||||||
5. CRUD livres (physique/numérique, statuts de lecture)
|
5. CRUD livres (physique/numérique, statuts de lecture)
|
||||||
6. Gestion des prêts
|
6. Gestion des prêts
|
||||||
7. Intégration scan caméra (**ZXing.Net** + interop caméra minimal)
|
7. Intégration scan caméra (**ZXing.Net** + interop caméra minimal)
|
||||||
8. Cache hors-ligne pour la consultation (voir « Stratégie hors-ligne »)
|
8. ~~Cache hors-ligne pour la consultation~~ — fait le 2026-08-18 (voir « Stratégie hors-ligne »)
|
||||||
9. Packaging YunoHost (`manifest.toml`, `conf/systemd.service`, `conf/nginx.conf`, `scripts/install`) en s'inspirant de radarr_ynh
|
9. Packaging YunoHost (`manifest.toml`, `conf/systemd.service`, `conf/nginx.conf`, `scripts/install`) en s'inspirant de radarr_ynh
|
||||||
|
|
||||||
## Questions ouvertes
|
## Questions ouvertes
|
||||||
@@ -556,3 +707,5 @@ Points à réévaluer en cours de route, sans blocage :
|
|||||||
- **AOT WASM** : mesurer le scan sur un vrai téléphone une fois fonctionnel. Activer `RunAOTCompilation` seulement si la fluidité est insuffisante.
|
- **AOT WASM** : mesurer le scan sur un vrai téléphone une fois fonctionnel. Activer `RunAOTCompilation` seulement si la fluidité est insuffisante.
|
||||||
- **Runner Gitea Actions** : à vérifier le jour où l'utilisateur voudra automatiser les releases.
|
- **Runner Gitea Actions** : à vérifier le jour où l'utilisateur voudra automatiser les releases.
|
||||||
- **Wikidata en 3ᵉ source ISBN** : uniquement si la cascade BnF → OpenLibrary montre ses limites en usage réel.
|
- **Wikidata en 3ᵉ source ISBN** : uniquement si la cascade BnF → OpenLibrary montre ses limites en usage réel.
|
||||||
|
- **Démarrage hors-ligne par le service worker** : à confirmer dans un navigateur ordinaire, l'environnement d'automatisation ne permettant pas d'enregistrer un service worker (voir la section dédiée). Le reste du hors-ligne, lui, est vérifié.
|
||||||
|
- **Taille de l'instantané** : 10 livres pèsent ~2,4 Ko de JSON. Rien à optimiser avant plusieurs milliers de fiches ; si le jour vient, découper par pages plutôt que renoncer à l'instantané complet, qui est ce qui rend la recherche hors-ligne possible.
|
||||||
|
|||||||
Reference in New Issue
Block a user