Bug de rafraichissement de données

This commit is contained in:
2026-07-05 10:50:17 +02:00
parent 186ffc2596
commit 0d8f92bc9a
4 changed files with 52 additions and 3 deletions
+13 -1
View File
@@ -23,8 +23,20 @@ server {
client_max_body_size 15M; client_max_body_size 15M;
} }
# SPA fallback # site.webmanifest — jamais mis en cache (sinon iOS garde l'ancienne
# config "Add to Home Screen" après un déploiement)
location = /site.webmanifest {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
}
# SPA fallback — index.html ne doit JAMAIS être mis en cache : la webapp
# installée sur l'écran d'accueil iPad (mode standalone) n'a ni bouton
# reload ni pull-to-refresh, donc si index.html reste en cache le
# WKWebView continue de charger l'ancien bundle après un déploiement.
location / { location / {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
} }
+20
View File
@@ -49,6 +49,26 @@ export default function App() {
}).catch(() => {}); }).catch(() => {});
}, []); }, []);
// Webapp installée sur l'écran d'accueil (iPad) : mode standalone, donc pas
// de bouton reload ni de pull-to-refresh natif. Si l'app est restée en
// arrière-plan un moment (cas typique : on la rouvre depuis l'icône des
// heures/jours après un déploiement serveur), on force un reload complet
// au retour au premier plan pour repartir sur un bundle et des données à jour.
useEffect(() => {
let hiddenAt = null;
const onVisibility = () => {
if (document.visibilityState === 'hidden') {
hiddenAt = Date.now();
} else if (document.visibilityState === 'visible' && hiddenAt) {
const awayMs = Date.now() - hiddenAt;
hiddenAt = null;
if (awayMs > 2 * 60 * 1000) window.location.reload();
}
};
document.addEventListener('visibilitychange', onVisibility);
return () => document.removeEventListener('visibilitychange', onVisibility);
}, []);
return ( return (
<Routes> <Routes>
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
+2 -2
View File
@@ -34,7 +34,7 @@ export const api = {
const qs = params ? '?' + new URLSearchParams( const qs = params ? '?' + new URLSearchParams(
Object.entries(params).filter(([, v]) => v !== undefined && v !== null && v !== '') Object.entries(params).filter(([, v]) => v !== undefined && v !== null && v !== '')
).toString() : ''; ).toString() : '';
return fetch(BASE + path + qs, { headers: authHeaders() }).then(handle); return fetch(BASE + path + qs, { headers: authHeaders(), cache: 'no-store' }).then(handle);
}, },
post: (path, body) => post: (path, body) =>
fetch(BASE + path, { fetch(BASE + path, {
@@ -65,7 +65,7 @@ export const api = {
postForm: (path, formData) => postForm: (path, formData) =>
fetch(BASE + path, { method: 'POST', body: formData, headers: authHeaders() }).then(handle), fetch(BASE + path, { method: 'POST', body: formData, headers: authHeaders() }).then(handle),
blob: (path) => blob: (path) =>
fetch(BASE + path, { headers: authHeaders() }).then(async res => { fetch(BASE + path, { headers: authHeaders(), cache: 'no-store' }).then(async res => {
if (!res.ok) { const t = await res.text(); throw new Error(t || res.statusText); } if (!res.ok) { const t = await res.text(); throw new Error(t || res.statusText); }
return res.blob(); return res.blob();
}), }),
+17
View File
@@ -25,6 +25,9 @@ function IconAide() {
function IconComm() { function IconComm() {
return <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>; return <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>;
} }
function IconRefresh() {
return <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>;
}
function IconChevronRight() { function IconChevronRight() {
return <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M9 18l6-6-6-6"/></svg>; return <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M9 18l6-6-6-6"/></svg>;
} }
@@ -192,6 +195,17 @@ export default function UserMenu() {
const go = (path) => { closeMenu(); navigate(path); }; const go = (path) => { closeMenu(); navigate(path); };
const handleLogout = () => { closeMenu(); logout(); navigate('/login'); }; const handleLogout = () => { closeMenu(); logout(); navigate('/login'); };
// En mode standalone (webapp ajoutée à l'écran d'accueil iPad), il n'y a
// ni bouton reload ni pull-to-refresh natif. On force donc une vraie
// navigation réseau (pas juste reload()) avec un paramètre anti-cache,
// pour être certain de récupérer l'index.html et les données à jour.
const handleForceRefresh = () => {
closeMenu();
const url = new URL(window.location.href);
url.searchParams.set('_r', Date.now().toString());
window.location.replace(url.toString());
};
const selectView = (v) => { const selectView = (v) => {
setActiveView(v); setActiveView(v);
closeMenu(); closeMenu();
@@ -268,6 +282,9 @@ export default function UserMenu() {
<button className="user-menu-item" role="menuitem" onClick={() => go('/aide')}> <button className="user-menu-item" role="menuitem" onClick={() => go('/aide')}>
<IconAide /> Aide <IconAide /> Aide
</button> </button>
<button className="user-menu-item" role="menuitem" onClick={handleForceRefresh}>
<IconRefresh /> Recharger l'application
</button>
<div className="user-menu-sep" /> <div className="user-menu-sep" />