From dbb103f6dbb8bd2bba245e150b7f39aab0c5e515 Mon Sep 17 00:00:00 2001 From: mathieu Date: Thu, 10 Sep 2026 17:58:27 +0200 Subject: [PATCH] =?UTF-8?q?-=20Ajout=20d'un=20bouton=20pour=20forcer=20la?= =?UTF-8?q?=20mise=20=C3=A0=20jour=20(si=20coupure=20internet=20pdt=20le?= =?UTF-8?q?=20dl,=20il=20faut=20tout=20d=C3=A9sinstaller=20et=20re-instalt?= =?UTF-8?q?er...).=20-=20Pour=20les=20grandes=20bibli,=20ajouter=20un=20vi?= =?UTF-8?q?suel=20que=20l'appli=20est=20en=20train=20de=20charger=20les=20?= =?UTF-8?q?donn=C3=A9es=20en=20cache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MaBibli.Client/Pages/APropos.razor | 52 +++++++ MaBibli.Client/Pages/Catalogue.razor | 2 +- MaBibli.Client/wwwroot/css/app.css | 41 +---- MaBibli.Client/wwwroot/index.html | 4 +- MaBibli.Client/wwwroot/js/forcer-maj.js | 21 +++ MaBibli.Client/wwwroot/js/mise-a-jour.js | 146 +++--------------- .../wwwroot/service-worker.published.js | 20 +-- 7 files changed, 108 insertions(+), 178 deletions(-) create mode 100644 MaBibli.Client/wwwroot/js/forcer-maj.js diff --git a/MaBibli.Client/Pages/APropos.razor b/MaBibli.Client/Pages/APropos.razor index ebc7eb4..ca62994 100644 --- a/MaBibli.Client/Pages/APropos.razor +++ b/MaBibli.Client/Pages/APropos.razor @@ -1,6 +1,7 @@ @page "/a-propos" @inject ServiceLivresApi Api @inject EtatReseau Reseau +@inject IJSRuntime JS @implements IDisposable @* @@ -93,6 +94,34 @@ celle qui y tourne en ce moment.

} + + @* + ⚠️ Ce numéro vient du SERVEUR (voir plus haut) : il ne prouve pas que le navigateur a + lui-même récupéré la nouvelle version. Le service worker n'active un nouveau cache + qu'une fois TOUS les onglets fermés — un appareil qui reste ouvert en arrière-plan + (téléphone surtout) peut rester bloqué sur l'ancienne version indéfiniment. Ce bouton + court-circuite cette attente : désinscrire le worker et vider son cache, recharger. + Hors-ligne il est désactivé — le déclencher sans réseau couperait l'accès hors-ligne + en cours, sans rien pouvoir retélécharger à la place. + *@ +

+ @if (Reseau.EnLigne) + { + + } + else + { + + } +

+

+ Vide le cache de l'application et la recharge entièrement — à utiliser si une nouvelle + version tarde à apparaître malgré la fermeture de tous les onglets. +

} else { @@ -145,6 +174,7 @@ private const string Depot = "https://git.akbar.nohost.me/mathieu/mabibli"; private VersionApplication? _version; + private bool _forcageEnCours; protected override async Task OnInitializedAsync() { @@ -161,6 +191,28 @@ StateHasChanged(); }); + /// + /// Désinscrit le service worker et vide son cache avant de recharger — voir js/forcer-maj.js. + /// Le bouton reste désactivé pendant l'opération : un double clic déclencherait deux + /// désinscriptions concurrentes pour un seul rechargement. + /// + private async Task ForcerMiseAJourAsync() + { + _forcageEnCours = true; + try + { + await using var module = await JS.InvokeAsync("import", "./js/forcer-maj.js"); + await module.InvokeVoidAsync("forcerMiseAJour"); + } + catch + { + // Le rechargement forcé qui suit normalement l'appel JS ne survient pas ici : on + // rend la main pour que l'utilisateur puisse réessayer plutôt que de rester sur un + // bouton grisé pour rien. + _forcageEnCours = false; + } + } + private static string Quand(DateTimeOffset instant) { var local = instant.ToLocalTime(); diff --git a/MaBibli.Client/Pages/Catalogue.razor b/MaBibli.Client/Pages/Catalogue.razor index 315124a..d6cb24d 100644 --- a/MaBibli.Client/Pages/Catalogue.razor +++ b/MaBibli.Client/Pages/Catalogue.razor @@ -228,7 +228,7 @@ @if (_chargement && _livres is null) { -

Chargement…

+ } else if (_livres is { Count: 0 }) { diff --git a/MaBibli.Client/wwwroot/css/app.css b/MaBibli.Client/wwwroot/css/app.css index 77e91f5..67b1deb 100644 --- a/MaBibli.Client/wwwroot/css/app.css +++ b/MaBibli.Client/wwwroot/css/app.css @@ -1180,41 +1180,6 @@ body { font-weight: 600; } -/* --- Mise à jour de l'application --- */ - -/* Injecté par js/mise-a-jour.js, hors composants Blazor : ce bandeau doit pouvoir s'afficher - même si l'application WebAssembly n'a pas démarré. D'où une règle globale et non scopée. - Il existe parce que les empreintes des assets WASM sont désactivées (CLAUDE.md) : le service - worker est le seul cache-busting du projet, et sa mise à jour attendrait sinon la fermeture - de tous les onglets. */ -#mb-maj { - position: fixed; - left: 0; - right: 0; - bottom: 0; - z-index: 1100; - display: flex; - gap: 0.75rem; - align-items: center; - justify-content: center; - flex-wrap: wrap; - padding: 0.7rem 1rem; - background: #1b3a5c; - color: #fff; - font-size: 0.9rem; -} - -#mb-maj button { - min-height: 2.5rem; - padding: 0.4rem 0.9rem; - border: 0; - border-radius: 6px; - background: #f5d76e; - color: #4a3800; - font-weight: 600; - cursor: pointer; -} - /* Un ISBN se relit chiffre à chiffre : une chasse fixe évite de confondre 1 et l, 0 et O, et rend les tranches séparées par des tirets alignées d'une ligne à l'autre. */ /* ⚠️ PAS de « white-space: nowrap » ici, et ce n'est pas un oubli. Un ISBN à tirets mesure @@ -1263,7 +1228,11 @@ body { display: flex; flex-wrap: wrap; gap: 0.4rem; - flex: 0 0 auto; + /* flex-shrink à 0 empêchait ce bloc de rétrécir : sur un écran étroit, il gardait la + largeur de ses boutons mis bout à bout et débordait à droite au lieu de les enrouler + sur une seconde ligne, malgré le flex-wrap ci-dessus. */ + flex: 1 1 auto; + min-width: 0; } /* --- Liste d'envies : ordre choisi --- */ diff --git a/MaBibli.Client/wwwroot/index.html b/MaBibli.Client/wwwroot/index.html index 46a39b7..766f845 100644 --- a/MaBibli.Client/wwwroot/index.html +++ b/MaBibli.Client/wwwroot/index.html @@ -43,8 +43,8 @@ + navigator.serviceWorker (absent hors contexte sécurisé), et journalise un échec au lieu + de l'avaler. Comportement de mise à jour par défaut de Blazor : voir js/mise-a-jour.js. --> diff --git a/MaBibli.Client/wwwroot/js/forcer-maj.js b/MaBibli.Client/wwwroot/js/forcer-maj.js new file mode 100644 index 0000000..cc5b99f --- /dev/null +++ b/MaBibli.Client/wwwroot/js/forcer-maj.js @@ -0,0 +1,21 @@ +// Échappatoire manuelle au comportement par défaut du service worker (qui attend la fermeture +// de TOUS les onglets avant d'activer une nouvelle version — voir js/mise-a-jour.js) : on +// désinscrit le worker et on vide le Cache Storage qu'il a rempli, puis on recharge. La page +// suivante repart de zéro et réenregistre un worker propre sur les fichiers actuels du serveur. +// +// ⚠️ Ne touche PAS à IndexedDB (cache-hors-ligne.js) : ces instantanés sont les données de +// lecture hors-ligne de l'utilisateur, pas du code périmé — les effacer n'aiderait à rien ici +// et ferait perdre la seule consultation possible sans réseau. +export async function forcerMiseAJour() { + if ('serviceWorker' in navigator) { + const enregistrements = await navigator.serviceWorker.getRegistrations(); + await Promise.all(enregistrements.map(enregistrement => enregistrement.unregister())); + } + + if ('caches' in self) { + const cles = await caches.keys(); + await Promise.all(cles.map(cle => caches.delete(cle))); + } + + window.location.reload(); +} diff --git a/MaBibli.Client/wwwroot/js/mise-a-jour.js b/MaBibli.Client/wwwroot/js/mise-a-jour.js index cd4cce8..35418dc 100644 --- a/MaBibli.Client/wwwroot/js/mise-a-jour.js +++ b/MaBibli.Client/wwwroot/js/mise-a-jour.js @@ -1,22 +1,24 @@ -// Enregistrement du service worker, application automatique au démarrage, bandeau en secours. +// Enregistrement du service worker — comportement par défaut de Blazor : un nouveau worker +// attend que tous les onglets de l'application soient fermés avant de prendre la main. Pas de +// SKIP_WAITING, pas de rechargement forcé, pas de bandeau : la mise à jour a lieu au prochain +// lancement de l'application, silencieusement. // -// Pourquoi ce fichier existe (et pourquoi il n'est pas qu'une ligne `register(...)`) : +// ⚠️ Revenu en arrière le 2026-09-10 : le mécanisme précédent (application automatique dans les +// 10 premières secondes, sinon bandeau « Mettre à jour ») ne se montrait pas fiable en pratique +// (constaté sur l'application installée, PC comme téléphone : la version affichée dans « À +// propos » — qui vient d'un appel au serveur, donc toujours à jour — changeait sans que le reste +// de l'application suive). Le comportement par défaut est plus simple à raisonner ; s'il rebloque +// comme en août 2026, le correctif portera sur un diagnostic visible avant de complexifier à +// nouveau ce fichier. // -// 1. Les empreintes des assets WASM sont DÉSACTIVÉES (voir CLAUDE.md). `blazor.webassembly.js` -// et `dotnet.js` portent donc des noms stables, et c'est le service worker — lui seul — qui -// empêche de servir éternellement une version périmée. Le mécanisme de Blazor fonctionne, -// mais il est silencieux et différé : le 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. -// -// 2. `navigator.serviceWorker` n'existe pas en contexte non sécurisé (http sur une IP locale). +// Ce qui reste, et pourquoi : +// 1. `navigator.serviceWorker` n'existe pas en contexte non sécurisé (http sur une IP locale). // L'appeler sans vérification lève une TypeError qui casse le script — et fait croire à un // défaut de la PWA alors que c'est le contexte qui n'est pas éligible. -// -// ⚠️ Le clic n'est plus obligatoire (décidé le 2026-08-20, après un blocage constaté sur -// l'application installée sur téléphone). CLAUDE.md actait « jamais tout seul » pour ne pas -// recharger sous une saisie en cours ; la raison reste valable, mais elle ne vaut que pour une -// session déjà entamée. Voir FENETRE_DEMARRAGE ci-dessous. +// 2. Chemin ABSOLU, et scope explicite. `register('service-worker.js')` se résout contre l'URL +// du DOCUMENT et non contre : ouvrir l'application sur une route profonde +// (/souhaits, /livres/3) visait /souhaits/service-worker.js, que le serveur ne sert pas — +// l'enregistrement échouait alors silencieusement, et avec lui tout le hors-ligne. (function () { 'use strict'; @@ -28,121 +30,13 @@ return; } - // Fenêtre pendant laquelle une nouvelle version s'applique TOUTE SEULE, sans rien demander. - // Au-delà, l'utilisateur est en train de faire quelque chose (une fiche à moitié saisie, un - // prêt en cours d'enregistrement) : on repasse par le bandeau, pour ne pas lui arracher son - // travail. Ouvrir l'application depuis l'écran d'accueil du téléphone tombe toujours dans - // cette fenêtre — c'est exactement le cas qui ne se mettait plus à jour. - var FENETRE_DEMARRAGE = 10000; - - // Si la reprise en main n'a pas lieu, on recharge quand même : le pire scénario est de - // laisser l'utilisateur devant un bouton grisé qui ne fait rien. - var DELAI_REPRISE = 5000; - - // Une seule reprise forcée par session : si la nouvelle version n'arrive pas à s'activer, - // recharger en boucle transformerait une mise à jour ratée en application inutilisable. - var CLE_FORCE = 'mb-maj-forcee'; - - var demandee = false; - var recharge = false; - - function recharger() { - if (recharge) return; - recharge = true; - window.location.reload(); - } - - function appliquer(enAttente) { - demandee = true; - // Le worker en attente prend la main sans qu'on ait à fermer tous les onglets. - enAttente.postMessage({ type: 'SKIP_WAITING' }); - - // Filet. `controllerchange` devrait suivre ; s'il ne vient pas (worker en attente issu - // d'une version antérieure au gestionnaire SKIP_WAITING, reprise en main qui échoue), - // rien ne se passerait du tout et le bouton resterait grisé pour toujours. - window.setTimeout(function () { - if (recharge) return; - var dejaForcee = false; - try { - dejaForcee = window.sessionStorage.getItem(CLE_FORCE) === '1'; - window.sessionStorage.setItem(CLE_FORCE, '1'); - } catch (e) { - // Navigation privée ou stockage refusé : on préfère ne pas forcer plutôt que - // risquer une boucle de rechargement qu'on ne saurait plus arrêter. - dejaForcee = true; - } - if (!dejaForcee) recharger(); - }, DELAI_REPRISE); - } - - function proposerLaMiseAJour(enAttente) { - if (document.getElementById('mb-maj')) return; - - var barre = document.createElement('div'); - barre.id = 'mb-maj'; - barre.setAttribute('role', 'status'); - barre.textContent = 'Une nouvelle version de MaBibli est disponible. '; - - var bouton = document.createElement('button'); - bouton.type = 'button'; - bouton.textContent = 'Mettre à jour'; - bouton.addEventListener('click', function () { - bouton.disabled = true; - bouton.textContent = 'Mise à jour…'; - appliquer(enAttente); - }); - - barre.appendChild(bouton); - document.body.appendChild(barre); - } - - // Au démarrage : on applique sans demander. Plus tard : on propose. - function traiter(enAttente) { - if (performance.now() < FENETRE_DEMARRAGE) { - appliquer(enAttente); - } else { - proposerLaMiseAJour(enAttente); - } - } - - // ⚠️ Chemin ABSOLU, et scope explicite. `register('service-worker.js')` se résout contre - // l'URL du DOCUMENT et non contre : ouvrir l'application sur une route - // profonde (/souhaits, /livres/3) visait /souhaits/service-worker.js, que le serveur ne sert - // pas — l'enregistrement échouait alors silencieusement, et avec lui tout le hors-ligne. + // updateViaCache: 'none' — sans lien avec le comportement d'application (ci-dessus) : ceci + // évite seulement qu'un cache HTTP intermédiaire masque une nouvelle version du script lui- + // même, y compris service-worker-assets.js qu'il importe. navigator.serviceWorker.register('/service-worker.js', { scope: '/', updateViaCache: 'none' }) - .then(function (enregistrement) { - // Un worker déjà installé attendait peut-être depuis la visite précédente. - if (enregistrement.waiting && navigator.serviceWorker.controller) { - traiter(enregistrement.waiting); - } - - enregistrement.addEventListener('updatefound', function () { - var nouveau = enregistrement.installing; - if (!nouveau) return; - - nouveau.addEventListener('statechange', function () { - // `controller` non nul = ce n'est pas la première installation, donc il y a - // bien une version précédente à remplacer. - if (nouveau.state === 'installed' && navigator.serviceWorker.controller) { - traiter(nouveau); - } - }); - }); - - // Vérification explicite à chaque chargement : ne pas dépendre du seul rythme - // interne du navigateur pour découvrir une version plus récente. - enregistrement.update().catch(function () { /* hors-ligne : sans objet */ }); - }) .catch(function (erreur) { // Ne jamais laisser cet échec passer inaperçu : sans service worker, l'application // ne démarre pas hors-ligne, et le cache IndexedDB ne sert alors à rien. console.error("Échec de l'enregistrement du service worker :", erreur); }); - - // Le rechargement n'a lieu que si la mise à jour a été demandée — automatiquement ou par un - // clic. Un changement de contrôleur survient aussi à la toute première installation, et - // recharger la page à ce moment-là serait un clignotement inexplicable. - navigator.serviceWorker.addEventListener('controllerchange', function () { - if (demandee) recharger(); - }); })(); diff --git a/MaBibli.Client/wwwroot/service-worker.published.js b/MaBibli.Client/wwwroot/service-worker.published.js index 1d16a12..f637465 100644 --- a/MaBibli.Client/wwwroot/service-worker.published.js +++ b/MaBibli.Client/wwwroot/service-worker.published.js @@ -6,15 +6,10 @@ self.addEventListener('install', event => event.waitUntil(onInstall(event))); self.addEventListener('activate', event => event.waitUntil(onActivate(event))); self.addEventListener('fetch', event => event.respondWith(onFetch(event))); -// Mise à jour à la demande. Sans cela, un nouveau worker attend que TOUS les onglets de -// l'application soient fermés — un onglet oublié fige indéfiniment l'utilisateur sur l'ancienne -// version. C'est d'autant plus important ici que les empreintes des assets WASM sont désactivées -// (voir CLAUDE.md) : ce worker est le SEUL mécanisme de cache-busting du projet. -// Le message ne vient que de js/mise-a-jour.js, après un clic explicite : jamais tout seul, pour -// ne pas mélanger deux versions au milieu d'une session. -self.addEventListener('message', event => { - if (event.data && event.data.type === 'SKIP_WAITING') self.skipWaiting(); -}); +// Comportement par défaut : pas de SKIP_WAITING. Un nouveau worker attend que TOUS les onglets +// de l'application soient fermés avant de prendre la main — la mise à jour a lieu au prochain +// lancement, jamais au milieu d'une session. Revenu en arrière le 2026-09-10 après un mécanisme +// de mise à jour immédiate qui ne se montrait pas fiable en pratique (voir js/mise-a-jour.js). const cacheNamePrefix = 'offline-cache-'; const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`; @@ -46,10 +41,9 @@ async function onActivate(event) { .filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName) .map(key => caches.delete(key))); - // Prendre la main sur les pages déjà ouvertes. skipWaiting() est censé le faire seul, mais - // c'est ce qui déclenche `controllerchange`, donc le rechargement côté page : sans reprise - // effective, le bouton « Mettre à jour » se grise et il ne se passe plus rien — symptôme - // observé sur téléphone. Cet appel est sans effet quand la reprise a déjà eu lieu. + // Sans effet sur les onglets déjà ouverts avant cette activation (ils ont fermé pour qu'elle + // ait lieu) ; utile seulement pour la toute première installation, où aucun worker ne + // contrôlait encore la page. await self.clients.claim(); }