@movalib/movalib-commons 1.68.32 → 1.68.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,237 @@
1
+ # Diagnostic & Fix — 404 à la navigation après useNavigationRefresh
2
+
3
+ ## Symptôme
4
+
5
+ Depuis l'implémentation de `useNavigationRefresh`, naviguer vers une route SPA (`/factures`, `/clients`, etc.) affiche la page 404. Rester sur la 404 et changer l'URL ne résout pas le problème — seul un reload complet depuis `/` le fait.
6
+
7
+ ---
8
+
9
+ ## Analyse du symptôme
10
+
11
+ Ce comportement confirme **deux problèmes simultanés** :
12
+
13
+ ### Problème 1 — CloudFront ne redirige pas les 404 vers `index.html` (cause racine)
14
+
15
+ Quand `useNavigationRefresh` fait `window.location.href = "/factures"`, il envoie une **vraie requête HTTP** vers S3. S3 n'a pas de fichier `/factures` → retourne une 404 → CloudFront sert `404.html`. L'app React est déchargée. On est maintenant sur une page HTML statique, plus dans la SPA. Changer l'URL dans la barre d'adresse envoie de nouvelles requêtes HTTP → toujours 404.
16
+
17
+ **Sans une custom error response CloudFront (`404 → /index.html` avec status 200), tout hard refresh sur une route SPA est fatal.**
18
+
19
+ ### Problème 2 — `useNavigationRefresh` se déclenche trop tôt (cause secondaire)
20
+
21
+ Si `__BUILD_TIME__` (injecté par Vite au build) ne matche pas exactement le `buildTime` dans `version.json` (écrit par `generate-version.js`), `useVersionCheck` détecte une "nouvelle version" dès le premier poll après le mount → `swUpdateFound` fire → `updatePending.current = true` → **toutes les navigations** deviennent des hard refreshes, même sans vrai changement de version.
22
+
23
+ ---
24
+
25
+ ## Travail demandé
26
+
27
+ ### Partie 1 — Fix infra CloudFront (à faire en PREMIER, manuellement)
28
+
29
+ > ⚠️ Cette partie n'est pas du code. Elle doit être exécutée en ligne de commande **avant** tout fix client. Sans elle, le fix client ne sert à rien.
30
+
31
+ **Étape 1 — Récupérer la config actuelle**
32
+ ```bash
33
+ aws cloudfront get-distribution-config --id E2ILRWTFEVONPB > /tmp/dist-config.json
34
+ cat /tmp/dist-config.json | python3 -c "import sys,json; d=json.load(sys.stdin); print('ETag:', d['ETag']); print('CustomErrorResponses:', json.dumps(d['DistributionConfig'].get('CustomErrorResponses', {}), indent=2))"
35
+ ```
36
+
37
+ **Étape 2 — Vérifier si la custom error response existe déjà**
38
+
39
+ Si la sortie contient un objet avec `"ErrorCode": 404` et `"ResponsePagePath": "/index.html"` → la config existe, passer à la partie 2.
40
+
41
+ Si `CustomErrorResponses` est vide ou absent → ajouter la règle :
42
+
43
+ ```bash
44
+ # Extraire l'ETag (affiché par la commande ci-dessus)
45
+ ETAG="EXXXXXXXXXX" # remplacer par la vraie valeur
46
+
47
+ # Extraire et modifier la config
48
+ python3 << 'EOF'
49
+ import json
50
+
51
+ with open('/tmp/dist-config.json') as f:
52
+ data = json.load(f)
53
+
54
+ config = data['DistributionConfig']
55
+
56
+ # Ajouter la custom error response SPA
57
+ config['CustomErrorResponses'] = {
58
+ "Quantity": 2,
59
+ "Items": [
60
+ {
61
+ "ErrorCode": 404,
62
+ "ResponsePagePath": "/index.html",
63
+ "ResponseCode": "200",
64
+ "ErrorCachingMinTTL": 0
65
+ },
66
+ {
67
+ "ErrorCode": 403,
68
+ "ResponsePagePath": "/index.html",
69
+ "ResponseCode": "200",
70
+ "ErrorCachingMinTTL": 0
71
+ }
72
+ ]
73
+ }
74
+
75
+ with open('/tmp/dist-config-updated.json', 'w') as f:
76
+ json.dump(config, f, indent=2)
77
+
78
+ print("✅ Config mise à jour dans /tmp/dist-config-updated.json")
79
+ EOF
80
+
81
+ # Appliquer
82
+ aws cloudfront update-distribution \
83
+ --id E2ILRWTFEVONPB \
84
+ --if-match "$ETAG" \
85
+ --distribution-config file:///tmp/dist-config-updated.json
86
+ ```
87
+
88
+ > Le 403 est ajouté en plus du 404 : S3 peut retourner 403 (au lieu de 404) sur certaines configs de bucket policy. Les deux doivent être couverts.
89
+ > `ErrorCachingMinTTL: 0` est important : sans ça CloudFront peut mettre en cache les 404 et les re-servir pendant plusieurs minutes après le fix.
90
+
91
+ ---
92
+
93
+ ### Partie 2 — Diagnostic dans le code
94
+
95
+ #### 2a. Vérifier l'alignement `__BUILD_TIME__` ↔ `version.json`
96
+
97
+ Dans `generate-version.js`, récupérer la valeur exacte écrite dans `buildTime`.
98
+ Dans `vite.config.ts`, récupérer la valeur exacte injectée dans `__BUILD_TIME__`.
99
+
100
+ **Les deux doivent être issues de la même source, au même moment du build.** Le pattern correct :
101
+
102
+ ```js
103
+ // generate-version.js — DOIT s'exécuter AVANT vite build
104
+ // et écrire le même timestamp que celui lu par vite.config
105
+
106
+ const buildTime = new Date().toISOString();
107
+ // écrire buildTime dans public/version.json
108
+ ```
109
+
110
+ ```ts
111
+ // vite.config.ts
112
+ import { execSync } from 'child_process';
113
+
114
+ // Lire le fichier version.json écrit par generate-version.js
115
+ // NE PAS recalculer un nouveau Date.now() ici → ça crée un mismatch
116
+ import versionJson from './public/version.json';
117
+
118
+ export default defineConfig({
119
+ define: {
120
+ __BUILD_TIME__: JSON.stringify(versionJson.buildTime),
121
+ }
122
+ })
123
+ ```
124
+
125
+ Si `vite.config.ts` recalcule sa propre date au lieu de lire `version.json` → les deux valeurs seront toujours différentes → `swUpdateFound` fire systématiquement → **corriger cet alignement en priorité**.
126
+
127
+ #### 2b. Ajouter un log temporaire de diagnostic dans `useVersionCheck.ts`
128
+
129
+ Ajouter temporairement (à retirer après validation) :
130
+
131
+ ```ts
132
+ if (buildTime !== __BUILD_TIME__ && buildTime !== dismissedVersion) {
133
+ // LOG TEMPORAIRE — retirer après diagnostic
134
+ console.warn('[useVersionCheck] Mismatch détecté', {
135
+ buildTime,
136
+ __BUILD_TIME__,
137
+ dismissedVersion,
138
+ match: buildTime === __BUILD_TIME__
139
+ });
140
+ updateDetected.current = true;
141
+ document.dispatchEvent(new CustomEvent("swUpdateFound", { detail: { buildTime } }));
142
+ }
143
+ ```
144
+
145
+ Ce log permettra de voir en console si `swUpdateFound` fire immédiatement au mount (mismatch permanent) ou seulement après un vrai déploiement.
146
+
147
+ ---
148
+
149
+ ### Partie 3 — Renforcer `useNavigationRefresh.ts`
150
+
151
+ Le hook doit être rendu **défensif** contre les cas dégénérés :
152
+
153
+ ```ts
154
+ import { useEffect, useRef } from "react";
155
+ import { useHistory } from "react-router-dom";
156
+
157
+ export function useNavigationRefresh() {
158
+ const history = useHistory();
159
+ const updatePending = useRef(false);
160
+ // Cooldown : si un refresh a déjà été tenté récemment, ne pas reboucler
161
+ const lastRefreshAttempt = useRef(0);
162
+ const REFRESH_COOLDOWN = 30_000; // 30s
163
+
164
+ useEffect(() => {
165
+ const onUpdate = () => {
166
+ console.log('[useNavigationRefresh] swUpdateFound reçu — refresh activé à la prochaine navigation');
167
+ updatePending.current = true;
168
+ };
169
+ const onDismiss = () => {
170
+ console.log('[useNavigationRefresh] swUpdateDismissed — refresh désactivé');
171
+ updatePending.current = false;
172
+ };
173
+
174
+ document.addEventListener("swUpdateFound", onUpdate);
175
+ document.addEventListener("swUpdateDismissed", onDismiss);
176
+
177
+ const unlisten = history.listen((location, action) => {
178
+ if (!updatePending.current) return;
179
+
180
+ // Anti-boucle : ne pas retenter un refresh dans les 30s
181
+ if (Date.now() - lastRefreshAttempt.current < REFRESH_COOLDOWN) {
182
+ console.warn('[useNavigationRefresh] Cooldown actif, refresh ignoré');
183
+ return;
184
+ }
185
+
186
+ // Pas de refresh sur bouton retour navigateur
187
+ if (action === "POP") return;
188
+
189
+ const destination = location.pathname + location.search + location.hash;
190
+ const current = window.location.pathname + window.location.search + window.location.hash;
191
+
192
+ // Pas de refresh si même URL (évite boucle sur lien actif)
193
+ if (destination === current) return;
194
+
195
+ console.log('[useNavigationRefresh] Hard refresh vers', destination);
196
+ lastRefreshAttempt.current = Date.now();
197
+ window.location.href = destination;
198
+ });
199
+
200
+ return () => {
201
+ document.removeEventListener("swUpdateFound", onUpdate);
202
+ document.removeEventListener("swUpdateDismissed", onDismiss);
203
+ unlisten();
204
+ };
205
+ }, [history]);
206
+ }
207
+ ```
208
+
209
+ ---
210
+
211
+ ### Partie 4 — Nettoyage après validation
212
+
213
+ Une fois que les logs de diagnostic confirment que tout fonctionne (le mismatch `__BUILD_TIME__` est corrigé, CloudFront redirige vers `index.html`, `useNavigationRefresh` ne fire que sur vrai changement de version) :
214
+
215
+ 1. Retirer les `console.log` / `console.warn` de diagnostic de `useVersionCheck.ts` et `useNavigationRefresh.ts`.
216
+ 2. Garder uniquement les `console.warn` d'anomalie (cooldown, etc.) en mode `process.env.NODE_ENV === 'development'`.
217
+
218
+ ---
219
+
220
+ ## Ordre d'exécution
221
+
222
+ 1. **Fix CloudFront** (Partie 1) — à faire manuellement en CLI, sans redéployer l'app.
223
+ 2. **Vérifier l'alignement `__BUILD_TIME__`** (Partie 2a) — corriger si nécessaire dans `vite.config.ts`.
224
+ 3. **Ajouter les logs temporaires** (Partie 2b) + **renforcer `useNavigationRefresh`** (Partie 3) — laisser Claude Code faire les modifications.
225
+ 4. **Redéployer** et vérifier en console que `swUpdateFound` ne fire pas immédiatement au mount.
226
+ 5. **Nettoyer** les logs une fois validé (Partie 4).
227
+
228
+ ---
229
+
230
+ ## Fichiers à modifier
231
+
232
+ | Fichier | Action |
233
+ |---|---|
234
+ | `vite.config.ts` | Vérifier que `__BUILD_TIME__` lit `version.json` au lieu de recalculer `Date.now()` |
235
+ | `src/hooks/useVersionCheck.ts` | Ajouter log temporaire de diagnostic du mismatch |
236
+ | `src/hooks/useNavigationRefresh.ts` | Ajouter cooldown anti-boucle + logs de diagnostic |
237
+ | CloudFront (CLI) | Ajouter custom error response 404+403 → `/index.html` (Partie 1, hors code) |
@@ -0,0 +1,157 @@
1
+ # Prompt Claude Code — Hard refresh à la navigation + nettoyage du système de version
2
+
3
+ ## Contexte
4
+
5
+ `movalib-pro` est une SPA React/TypeScript sous Vite, déployée sur S3 + CloudFront.
6
+
7
+ Le système de déploiement a été modifié pour **conserver les 3 dernières générations de chunks hashés** sur le bucket S3 (rétention générationnelle dans le script de deploy). En conséquence, les anciens chunks ne 404 plus immédiatement — mais un utilisateur peut rester sur une vieille version jusqu'à ce qu'il recharge la page.
8
+
9
+ La stratégie retenue pour forcer la mise à jour : **hard refresh silencieux au changement d'URL**, uniquement si une nouvelle version a été détectée. Pas de reload brutal en plein travail, pas de perte de saisie : le refresh est déclenché à l'instant où l'utilisateur navigue vers une nouvelle route, il atterrit directement sur la destination en dernière version.
10
+
11
+ ## Ce qui existe déjà (ne pas recréer)
12
+
13
+ ### `src/hooks/useVersionCheck.ts`
14
+ Poll `version.json` toutes les 5 minutes et au clic. Si `buildTime !== __BUILD_TIME__`, dispatche un `CustomEvent("swUpdateFound", { detail: { buildTime } })` sur `document`. Gestion du dismiss via `localStorage` (clé `swDismissedVersion`).
15
+
16
+ ```ts
17
+ declare const __BUILD_TIME__: string;
18
+ // → injecté par vite.config via define: { __BUILD_TIME__: ... }
19
+ ```
20
+
21
+ ### `src/utils/chunkErrorUtils.ts`
22
+ Contient `isChunkLoadError(error)`, `canAutoReload()` et `markReloadTimestamp()` — gestion du cooldown pour éviter les boucles de reload lors d'un chunk 404.
23
+
24
+ ### `src/utils/lazyWithRetry.ts`
25
+ Wrapper `React.lazy()` qui catch les erreurs de chunk et appelle `canAutoReload()` / `markReloadTimestamp()` avant de faire `window.location.reload()`.
26
+
27
+ ### `index.html`
28
+ Contient un listener inline `vite:preloadError` qui fait un `window.location.reload()` **sans garde anti-boucle** (voir problème 1 ci-dessous).
29
+
30
+ ### Snackbar / notification UI
31
+ Une snackbar MUI écoute `swUpdateFound` et affiche « Nouvelle version disponible » avec un bouton « Recharger ». Elle dispatche `swUpdateDismissed` si l'utilisateur ferme.
32
+
33
+ ### Router
34
+ `react-router-dom` v5 avec `BrowserRouter`. L'objet `history` est accessible via `useHistory()` hook ou via un `history` singleton créé avec `createBrowserHistory()`.
35
+
36
+ ---
37
+
38
+ ## Travail demandé
39
+
40
+ ### 1. Aligner la garde anti-boucle du listener `vite:preloadError` dans `index.html`
41
+
42
+ **Problème** : le listener inline fait un `reload()` inconditionnel. Si le reload tombe encore sur un index.html périmé, on boucle indéfiniment.
43
+
44
+ **Fix** : ajouter une garde basée sur `sessionStorage`, avec la **même clé et le même cooldown** que `chunkErrorUtils.ts` pour que les deux mécanismes partagent le même état. Récupère la clé et la durée de cooldown depuis `chunkErrorUtils.ts` et aligne le listener en conséquence.
45
+
46
+ ```html
47
+ <!-- index.html — remplacer le listener existant par : -->
48
+ <script>
49
+ window.addEventListener("vite:preloadError", function (event) {
50
+ event.preventDefault();
51
+ // ⚠️ clé et cooldown IDENTIQUES à ceux de chunkErrorUtils.ts
52
+ var KEY = "chunk-reload-ts"; // à aligner sur canAutoReload()
53
+ var COOLDOWN = 10000; // à aligner sur canAutoReload()
54
+ var last = Number(window.sessionStorage.getItem(KEY) || 0);
55
+ if (Date.now() - last > COOLDOWN) {
56
+ window.sessionStorage.setItem(KEY, String(Date.now()));
57
+ window.location.reload();
58
+ }
59
+ });
60
+ </script>
61
+ ```
62
+
63
+ Vérifie dans `chunkErrorUtils.ts` les valeurs exactes de la clé et du cooldown avant d'appliquer.
64
+
65
+ ---
66
+
67
+ ### 2. Ajouter le hard refresh à la navigation
68
+
69
+ **Objectif** : quand `swUpdateFound` a été reçu (nouvelle version détectée), intercepter le prochain changement de route react-router et faire un `window.location.href = destination` au lieu de laisser le routeur faire une navigation client.
70
+
71
+ **Règles impératives** :
72
+ - Le refresh est **conditionnel** : uniquement si une nouvelle version a été détectée. Jamais systématique.
73
+ - L'utilisateur atterrit sur **la page de destination**, pas sur la page courante (`window.location.href = to.pathname + to.search`, pas `window.location.reload()`).
74
+ - Ne jamais déclencher le refresh sur la **même URL** (évite une boucle si l'utilisateur clique sur un lien actif).
75
+ - Respecter le **dismiss** : si l'utilisateur a dismissé la notif (`swUpdateDismissed`), ne pas forcer le refresh non plus.
76
+
77
+ **Implémentation suggérée** — créer un hook `src/hooks/useNavigationRefresh.ts` :
78
+
79
+ ```ts
80
+ import { useEffect, useRef } from "react";
81
+ import { useHistory } from "react-router-dom";
82
+
83
+ export function useNavigationRefresh() {
84
+ const history = useHistory();
85
+ const updatePending = useRef(false);
86
+
87
+ useEffect(() => {
88
+ const onUpdate = () => { updatePending.current = true; };
89
+ const onDismiss = () => { updatePending.current = false; };
90
+
91
+ document.addEventListener("swUpdateFound", onUpdate);
92
+ document.addEventListener("swUpdateDismissed", onDismiss);
93
+
94
+ const unlisten = history.listen((location, action) => {
95
+ if (!updatePending.current) return;
96
+ if (action === "POP") return; // bouton retour navigateur → pas de reload
97
+ const destination = location.pathname + location.search + location.hash;
98
+ if (destination === window.location.pathname + window.location.search) return;
99
+ window.location.href = destination;
100
+ });
101
+
102
+ return () => {
103
+ document.removeEventListener("swUpdateFound", onUpdate);
104
+ document.removeEventListener("swUpdateDismissed", onDismiss);
105
+ unlisten();
106
+ };
107
+ }, [history]);
108
+ }
109
+ ```
110
+
111
+ Ce hook est à appeler **une seule fois**, au niveau du composant racine qui a accès au `Router` (ex. `App.tsx` ou le composant juste en dessous de `<BrowserRouter>`).
112
+
113
+ ---
114
+
115
+ ### 3. Nettoyer `useVersionCheck.ts`
116
+
117
+ - Ajouter l'écoute de `visibilitychange` pour déclencher un check immédiat quand l'utilisateur revient sur l'onglet (les timers setInterval sont throttlés en arrière-plan par le navigateur).
118
+ - Remplacer `fetch('/version.json?t=${Date.now()}')` par `fetch('/version.json', { cache: 'no-store' })` — plus propre, le `?t=` est redondant vu que le fichier est servi en `no-cache, no-store` côté S3.
119
+ - Ajouter un guard `if (!res.ok) return` avant `res.json()` pour éviter de tenter de parser une page d'erreur CloudFront (HTML) comme du JSON.
120
+ - Ajouter un `MIN_GAP` (ex. 30 secondes) pour éviter les rafales de fetch si `visibilitychange` + `setInterval` + `click` se chevauchent.
121
+ - Le `checkOnClick` peut être conservé si présent, mais il doit respecter le `MIN_GAP`.
122
+
123
+ ---
124
+
125
+ ### 4. Filtrer les ChunkLoadError dans le système de crash reporting
126
+
127
+ Dans `errorReportingService.ts`, avant d'envoyer une erreur, filtrer les erreurs de type chunk load pour ne pas polluer le crash reporting avec du bruit de déploiement qui se résout tout seul via le reload :
128
+
129
+ Les patterns à filtrer (tous navigateurs) :
130
+ - `Failed to fetch dynamically imported module` (Chrome)
131
+ - `error loading dynamically imported module` (Firefox)
132
+ - `Importing a module script failed` (Safari)
133
+ - `Unable to preload CSS` (CSS chunk manquant)
134
+ - `Cannot read properties of undefined (reading 'default')` **si** l'erreur provient d'un fichier `assets/index-*.js` (signature du chunk Vite)
135
+
136
+ Ces erreurs doivent être soit ignorées, soit loggées avec un niveau `warn` distinct (pas `error`) pour conserver une trace sans polluer les alertes.
137
+
138
+ ---
139
+
140
+ ## Fichiers à modifier / créer
141
+
142
+ | Fichier | Action |
143
+ |---|---|
144
+ | `index.html` | Modifier le listener `vite:preloadError` (garde anti-boucle) |
145
+ | `src/hooks/useVersionCheck.ts` | Nettoyer (visibilitychange, no-store, MIN_GAP, guard ok) |
146
+ | `src/hooks/useNavigationRefresh.ts` | Créer (hard refresh conditionnel à la navigation) |
147
+ | `src/App.tsx` (ou composant racine) | Appeler `useNavigationRefresh()` une seule fois |
148
+ | `src/services/errorReportingService.ts` | Filtrer les ChunkLoadError |
149
+
150
+ ---
151
+
152
+ ## Ce qu'il ne faut pas faire
153
+
154
+ - Ne pas faire `window.location.reload()` à chaque changement de route — uniquement si `updatePending === true`.
155
+ - Ne pas recréer un deuxième système de polling de version — `useVersionCheck` existant est la source de vérité.
156
+ - Ne pas modifier `lazyWithRetry.ts` ni `chunkErrorUtils.ts` sauf si l'alignement de clé de l'étape 1 l'exige.
157
+ - Ne pas supprimer la snackbar « Nouvelle version disponible » — elle reste utile pour les utilisateurs qui ne naviguent pas pendant une longue période.