Bug de rafraichissement de données
This commit is contained in:
+13
-1
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user