- Ajout d'un bouton pour forcer la mise à jour (si coupure internet pdt le dl, il faut tout désinstaller et re-instalter...).

- Pour les grandes bibli, ajouter un visuel que l'appli est en train de charger les données en cache
This commit is contained in:
mathieu
2026-09-10 17:58:27 +02:00
parent e2254e700d
commit dbb103f6db
7 changed files with 108 additions and 178 deletions
+52
View File
@@ -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.
</p>
}
@*
⚠️ 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.
*@
<p>
@if (Reseau.EnLigne)
{
<button type="button" class="bouton bouton-discret" disabled="@_forcageEnCours"
@onclick="ForcerMiseAJourAsync">
@(_forcageEnCours ? "Mise à jour…" : "Forcer la mise à jour")
</button>
}
else
{
<button type="button" class="bouton bouton-discret" disabled
title="@EtatReseau.MotifHorsLigne">Forcer la mise à jour</button>
}
</p>
<p class="message-discret">
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.
</p>
}
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();
});
/// <summary>
/// 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.
/// </summary>
private async Task ForcerMiseAJourAsync()
{
_forcageEnCours = true;
try
{
await using var module = await JS.InvokeAsync<IJSObjectReference>("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();
+1 -1
View File
@@ -228,7 +228,7 @@
@if (_chargement && _livres is null)
{
<p class="message-discret">Chargement…</p>
<Patience Message="Chargement du catalogue…" />
}
else if (_livres is { Count: 0 })
{
+5 -36
View File
@@ -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 --- */
+2 -2
View File
@@ -43,8 +43,8 @@
</div>
<script src="_framework/blazor.webassembly.js"></script>
<!-- Enregistrement du service worker déporté : il vérifie le support avant d'appeler
navigator.serviceWorker (absent hors contexte sécurisé), journalise un échec au lieu de
l'avaler, et propose la mise à jour quand une nouvelle version est prête. -->
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. -->
<script src="js/mise-a-jour.js"></script>
</body>
+21
View File
@@ -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();
}
+20 -126
View File
@@ -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 <base href="/"> : 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 <base href="/"> : 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();
});
})();
@@ -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();
}